Compare commits

...

739 Commits

Author SHA1 Message Date
88c2b90860 static analysis 2024-10-02 09:06:30 +03:00
c84d84ace2 static analysis 2024-10-02 08:56:33 +03:00
Tal
af5a50ac6a Merge pull request #1264 from Codium-ai/tr/commitable_alongside_table
dual publishing mode
2024-10-01 08:38:34 +03:00
bccc2844b9 dual publishing mode 2024-10-01 08:32:29 +03:00
Tal
d80103751e Merge pull request #1263 from Codium-ai/tr/commitable_alongside_table
feat: add dual publishing mode for PR code suggestions
2024-10-01 08:25:00 +03:00
8ff8b1d48e default 2024-10-01 08:22:28 +03:00
da0bd84746 dual 2024-10-01 08:20:16 +03:00
b42ded61f8 docs: add guidelines for implementing proposed code suggestions in improve.md 2024-10-01 08:16:51 +03:00
dfa4f22be2 feat: add dual publishing mode for PR code suggestions
- Introduced dual publishing mode to present high-scoring suggestions as both table entries and commitable PR comments.
- Updated documentation to include configuration options for dual publishing mode.
- Enhanced `pr_code_suggestions.py` to handle dual publishing logic and error handling.
- Modified `configuration.toml` to include `duel_publishing_score_threshold` setting.
2024-10-01 08:01:27 +03:00
Tal
7d55fc174b Merge pull request #1262 from Codium-ai/tr/prompt
style: refine field descriptions in KeyIssuesComponentLink model
2024-09-30 13:06:11 +03:00
968fb71577 refactor: update terminology for issue review recommendations in utils.py 2024-09-30 13:03:42 +03:00
454365913f refactor: update terminology for issue review recommendations in utils.py 2024-09-30 13:00:01 +03:00
bbaba2dbda refactor: update terminology for issue review recommendations in utils.py
style: refine field descriptions in KeyIssuesComponentLink model
2024-09-30 08:58:32 +03:00
e4c6792866 Merge remote-tracking branch 'origin/main' 2024-09-30 07:52:54 +03:00
183dd5d2fc diverse 2024-09-30 07:52:43 +03:00
Tal
2e79392a5f Merge pull request #1256 from Codium-ai/mrT23-patch-2
disable chat message for github action
2024-09-30 07:40:04 +03:00
da4148f336 docs: update links and references to Qodo Merge in documentation 2024-09-29 17:38:02 +03:00
070ed21103 update documentation links to qodo-merge-docs.qodo.ai 2024-09-29 17:32:16 +03:00
Tal
640bc1e28a Merge pull request #1261 from Codium-ai/tr/qodo_merge
Qodo Merge rename
2024-09-29 17:26:13 +03:00
6c4aa468a9 formerly 2024-09-29 17:24:37 +03:00
da20efd050 Qodo Merge rename 2024-09-29 17:19:46 +03:00
c25aaad176 Qodo Merge rename 2024-09-29 17:15:49 +03:00
Tal
b4c20d683c Merge pull request #1260 from Codium-ai/mrT23-patch-3
Update CNAME
2024-09-29 12:26:50 +03:00
Tal
7bc20d6f16 Update CNAME 2024-09-29 12:25:48 +03:00
f894e8831b revert unauthorized merge 2024-09-29 08:37:26 +03:00
25b7e1e777 update docs URL 2024-09-29 08:19:26 +03:00
Tal
6ba2fb212d Merge pull request #1259 from Codium-ai/tr/intro_review
Add intro text option for PR reviews in configuration and utils
2024-09-29 07:32:27 +03:00
4a60046f7c update tests 2024-09-29 07:28:02 +03:00
35b1f5e747 key 2024-09-29 07:23:34 +03:00
d77a819d92 Add intro text option for PR reviews in configuration and utils 2024-09-29 07:06:48 +03:00
f3fd439d47 docs 2024-09-27 16:24:07 +03:00
Tal
6188afff48 Merge pull request #1257 from Codium-ai/tr/code_suggestion_message
docs: update PR-Agent documentation with PR Chat search instructions
2024-09-27 16:15:09 +03:00
57cfdcf274 docs: update PR-Agent documentation with PR Chat search instructions 2024-09-27 16:13:54 +03:00
Tal
1333ac47bc Update configuration.toml 2024-09-26 18:24:11 +03:00
Tal
267e01409b Merge pull request #1255 from Codium-ai/tr/code_suggestion_message
Enable intro and chat text for PR code suggestions in configuration
2024-09-26 17:15:10 +03:00
8bdebcb99f Enable intro and chat text for PR code suggestions in configuration 2024-09-26 17:11:00 +03:00
Tal
a13400b9b8 Merge pull request #1254 from Codium-ai/tr/code_suggestion_message
Add intro and chat text options for PR code suggestions in configuration
2024-09-26 16:31:10 +03:00
89f9cf5adc Add intro and chat text options for PR code suggestions in configuration 2024-09-26 09:07:51 +03:00
Tal
6b653dbe48 Merge pull request #1253 from Codium-ai/tr/code_suggestion_message
Add configuration for auto actions in GitHub Action runner
2024-09-26 08:50:56 +03:00
109b965407 Add configuration for auto actions in GitHub Action runner 2024-09-26 08:03:39 +03:00
Tal
511c5a31db Merge pull request #1252 from Codium-ai/tr/prompts_refactor
improve code suggestion prompt
2024-09-25 21:23:00 +03:00
3dd8050004 improve code suggestion prompt 2024-09-25 21:22:41 +03:00
4b7d01972c improve code suggestion prompt 2024-09-25 21:15:14 +03:00
05ec944a8b improve code suggestion prompt 2024-09-25 17:52:54 +03:00
4713ae74b7 improve code suggestion prompt 2024-09-25 17:42:59 +03:00
c828cdde62 improve code suggestion prompt 2024-09-25 17:41:21 +03:00
6f14f9c8e1 improve code suggestion prompt 2024-09-25 16:22:16 +03:00
Tal
9f8cc75bd3 Merge pull request #1250 from chapeupreto/improve-pipeline-gitlab
docs: improve GitLab installation instruction
2024-09-25 07:11:54 +03:00
0668ccbb9e docs: improve GitLab installation instruction
This commit adds a missing `export` instruction that is required mainly
for self-hosted GitLab installations.
2024-09-24 23:34:40 -03:00
Tal
8a287f8ed6 Merge pull request #1248 from Codium-ai/tr/help_fixes
DocHelper
2024-09-22 16:17:30 +03:00
d5625db3c8 DocHelper 2024-09-22 16:16:59 +03:00
d6b779eef8 DocHelper 2024-09-22 16:14:32 +03:00
804cb9ec1d DocHelper 2024-09-22 15:55:18 +03:00
Tal
47d32283ca Merge pull request #1247 from Codium-ai/tr/help_fixes
Tr/help fixes
2024-09-22 10:23:29 +03:00
397963257d DocHelper 2024-09-22 10:21:47 +03:00
a3fd15bb92 Merge remote-tracking branch 'origin/main' into tr/help_fixes 2024-09-22 09:27:10 +03:00
ded7d96649 DocHelper 2024-09-22 09:23:34 +03:00
Tal
bbf06e25ef Merge pull request #1246 from Codium-ai/tr/docs_update
update docs
2024-09-22 09:20:38 +03:00
7e5ddf7e37 Update improve.md with enhanced self-review configuration details 2024-09-22 09:19:39 +03:00
0198c61cf7 update docs 2024-09-22 09:00:56 +03:00
20d8e76a7f logs 2024-09-22 08:31:56 +03:00
be8052251a Update pr_help_prompts.toml and fix snippet indexing in pr_help_message.py 2024-09-22 08:13:23 +03:00
ba08b13446 update PR-Agent usage instructions in pr_description.py 2024-09-21 21:10:51 +03:00
835684b92a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	pr_agent/tools/pr_description.py
2024-09-21 21:10:38 +03:00
90295b6429 update PR-Agent usage instructions in pr_description.py 2024-09-21 21:10:06 +03:00
Tal
08d6bbc94c Merge pull request #1245 from Codium-ai/tr/help_rag
docs pr help
2024-09-21 20:58:06 +03:00
8229d98842 docs pr help 2024-09-21 20:55:05 +03:00
Tal
9e28aca919 Merge pull request #1244 from Codium-ai/tr/help_rag
Refactor S3 file handling and update Dockerfile to include local Chroma DB file
2024-09-21 19:28:04 +03:00
3e780783cc Add docs db 2024-09-21 19:24:23 +03:00
5c7b65810c Refactor S3 file handling and update Dockerfile to include local Chroma DB file 2024-09-21 19:11:46 +03:00
Tal
f2f82e8805 Merge pull request #1243 from Codium-ai/tr/help_rag
get protection
2024-09-21 17:08:23 +03:00
1e51acff22 get protection 2024-09-21 17:07:46 +03:00
Tal
81e847e477 Merge pull request #1242 from Codium-ai/tr/help_rag
prompts
2024-09-21 16:59:23 +03:00
a70fe27d94 prompts 2024-09-21 16:58:37 +03:00
Tal
1a5835a947 Merge pull request #1241 from Codium-ai/tr/help_rag
Add PR help message functionality and update dependencies
2024-09-21 16:28:42 +03:00
4b74506107 Add PR help message functionality and update dependencies
- Implement PRHelpMessage class to provide AI-powered assistance for pull requests.
- Add methods for similarity search using local, S3, and Pinecone databases.
- Update `requirements.txt` to include new dependencies for langchain and chromadb.
- Modify `configuration.toml` to include `force_local_db` setting for PR help.
- Update `aiohttp` and `openai` package versions.
2024-09-21 16:22:51 +03:00
08319f8492 fixed azure remove comment bug 2024-09-19 12:54:26 +03:00
Tal
b447080777 Merge pull request #1236 from Codium-ai/tr/tldr
TLDR
2024-09-16 09:23:49 +03:00
da398ce56f TLDR 2024-09-16 09:21:52 +03:00
Tal
16763d81b4 Merge pull request #1232 from yzongyue/feature/compatible_with_old_gitlab
func get_line_link compatible with old gitlab versions
2024-09-15 17:06:43 +03:00
Tal
80fe297bc9 Merge pull request #1231 from matanbaruch/main
Update PR Action Handling Logic in GitHub Action Runner
2024-09-15 15:30:34 +03:00
5d68b0c492 Update automations_and_usage.md 2024-09-15 15:19:40 +03:00
Tal
8d5f015e5c Merge pull request #1234 from Codium-ai/tr/dynamic
self_reflection
2024-09-15 14:52:06 +03:00
be03f83318 self_reflection 2024-09-15 14:50:24 +03:00
cbfd250c0c self_reflection 2024-09-15 14:47:27 +03:00
Tal
7ce46e65a1 Merge pull request #1233 from Codium-ai/tr/dynamic
collapsible
2024-09-15 13:45:39 +03:00
600f230ba7 collapsible 2024-09-15 13:44:27 +03:00
Tal
4f4f13b8b2 Update improve.md 2024-09-15 12:44:11 +03:00
Tal
146b8823a9 Merge pull request #1230 from Codium-ai/tr/dynamic
Tr/dynamic
2024-09-15 12:39:00 +03:00
fdb1ff8057 update best practics 2024-09-15 12:36:00 +03:00
ce8e637800 get_line_link compatible with old gitlab versions 2024-09-15 14:56:08 +08:00
306af02d22 Update github_action_runner.py 2024-09-15 09:42:02 +03:00
a23541912b Update github_action_runner.py 2024-09-15 09:40:05 +03:00
0851767774 Update configuration.toml 2024-09-15 09:35:58 +03:00
585a7f1c69 Update github_action_runner.py 2024-09-15 09:33:47 +03:00
8d82cb2e04 f string 2024-09-15 08:50:24 +03:00
7586514abf docs: clarify guidelines on file length and specificity in improve.md 2024-09-15 08:28:01 +03:00
480a025877 Merge remote-tracking branch 'origin/main' into tr/dynamic 2024-09-15 08:08:11 +03:00
8f943a0d44 fix: update error logging messages and system prompt handling in litellm_ai_handler.py 2024-09-15 08:07:59 +03:00
Tal
2102c51422 Merge pull request #1228 from eddielu/eddie/azurecommentstatus
Set azure devops default status to ByDesign
2024-09-14 20:34:13 +03:00
29028d43cf Set azure devops default status to ByDesign 2024-09-13 18:01:08 -07:00
Tal
95d1b0d0c5 Merge pull request #1227 from Codium-ai/tr/dynamic
refactor logic
2024-09-13 22:22:47 +03:00
cc0e432247 refactor logic 2024-09-13 22:17:24 +03:00
0fb158fd47 fix push gitlab 2024-09-13 20:59:45 +03:00
Tal
867a430a38 Merge pull request #1226 from KennyDizi/main
Add new o1-mini and o1-preview models
2024-09-13 17:17:26 +03:00
a94496285f Add o1-preview models 2024-09-13 08:23:18 +07:00
567c144176 Add o1-mini models 2024-09-13 08:22:09 +07:00
Tal
c08b59a74d Merge pull request #1224 from Codium-ai/tr/dynamic_context_
Tr/dynamic context
2024-09-12 11:43:15 +03:00
0ba81e1ac7 docs: add dynamic context strategy documentation and update configuration settings 2024-09-12 11:42:27 +03:00
2cb0dd2496 docs: add dynamic context strategy documentation and update configuration settings 2024-09-12 11:38:54 +03:00
a8367d1a22 docs: add dynamic context strategy documentation and update configuration settings
- Added detailed documentation on the dynamic context strategy in `dynamic_context.md`.
- Updated configuration settings in `configuration.toml` to enable dynamic context by default.
- Adjusted context line parameters in `additional_configurations.md` to reflect new defaults.
- Announced dynamic context as the default option in the `README.md` news section.
2024-09-12 11:37:52 +03:00
Tal
1a3345c6e6 Merge pull request #1223 from Codium-ai/mrT23-patch-12
Update additional_configurations.md
2024-09-12 09:29:02 +03:00
Tal
564845adff Update additional_configurations.md 2024-09-12 09:27:45 +03:00
Tal
3ea691e70a Merge pull request #1216 from Codium-ai/tr/azure_parsing
fix: improve Azure DevOps PR URL parsing and add unit tests
2024-09-12 09:09:24 +03:00
Tal
5047d076f8 Merge pull request #1222 from Codium-ai/tr/docs_and_fixes
enhance: cap patch extra lines and update documentation with separato…
2024-09-12 09:07:06 +03:00
7de6bb0150 enhance: cap patch extra lines and update documentation with separators and context adjustments 2024-09-12 09:05:26 +03:00
a1582b5338 enhance: cap patch extra lines and update documentation with separators and context adjustments 2024-09-12 09:01:36 +03:00
Tal
dd8d78e7d8 Merge pull request #1221 from Codium-ai/tr/updates_and_fixes
FAQ
2024-09-11 16:53:41 +03:00
5af6cc7538 FAQ 2024-09-11 16:52:21 +03:00
Tal
6cc562d6a2 Merge pull request #1220 from Codium-ai/tr/updates_and_fixes
FAQ
2024-09-11 16:41:07 +03:00
19b051b992 FAQ 2024-09-11 16:40:34 +03:00
be68ee89f3 FAQ 2024-09-11 16:36:27 +03:00
db6c75a130 FAQ 2024-09-11 16:28:09 +03:00
Tal
74688846e0 Merge pull request #1219 from Codium-ai/tr/updates_and_fixes
docs and fixes
2024-09-10 20:10:53 +03:00
09b0a04a47 docs and fixes 2024-09-10 20:06:48 +03:00
Tal
cc1b65f886 Merge pull request #1218 from Codium-ai/tr/updates_and_fixes
feat: enhance error handling and logging, update AI metadata terminology
2024-09-10 17:59:48 +03:00
Tal
1451d82d6b Update pr_agent/algo/pr_processing.py
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-09-10 17:50:32 +03:00
01ba6fe63d feat: enhance error handling and logging, update AI metadata terminology
- Improved error handling and logging in `pr_processing.py` and `github_polling.py` to provide more detailed error information.
- Updated AI metadata terminology from "AI-generated file summary" to "AI-generated changes summary" across multiple files for consistency.
- Added a placeholder method `publish_file_comments` in `azuredevops_provider.py`.
- Refined logging messages in `azuredevops_provider.py` for better clarity.
2024-09-10 17:44:26 +03:00
Tal
74f9da1135 Merge pull request #1217 from Codium-ai/mrT23-patch-12
Update index.md
2024-09-10 09:06:55 +03:00
Tal
f80c2ae2c8 Update index.md 2024-09-10 09:05:50 +03:00
e444da8378 fix: improve Azure DevOps PR URL parsing and add unit tests 2024-09-10 08:19:22 +03:00
Tal
25ad8a09ce Merge pull request #1215 from Codium-ai/tr/docs
docs
2024-09-10 08:00:36 +03:00
897e791b1a docs 2024-09-10 08:00:10 +03:00
7f94dda54e docs 2024-09-10 07:58:18 +03:00
Tal
538a592882 Merge pull request #1214 from Codium-ai/tr/docs
docs
2024-09-10 07:53:35 +03:00
a3cb7277a7 docs 2024-09-10 07:51:47 +03:00
Tal
b5cd560402 Merge pull request #1212 from Codium-ai/tr/configurations
docs
2024-09-09 19:47:47 +03:00
fd38c33fcb docs 2024-09-09 19:45:29 +03:00
Tal
f767a3dfde Merge pull request #1211 from Codium-ai/hl/impact_docs
impact docs
2024-09-09 19:34:42 +03:00
9f8b619858 small fix 2024-09-09 18:47:53 +03:00
8de16939ba impact docs 2024-09-09 18:15:56 +03:00
Tal
6ed5537065 Merge pull request #1210 from Codium-ai/tr/configurations
bug fix: remove unused get_pr_description method from Azure DevOps
2024-09-09 16:26:46 +03:00
1a9638cf87 bug fix: remove unused get_pr_description method from Azure DevOps provider 2024-09-09 16:25:34 +03:00
Tal
49521aafff Merge pull request #1208 from Codium-ai/tr/configurations
config docs
2024-09-09 08:34:46 +03:00
c8e8ed89d2 feat: integrate Dynaconf for configuration management and enhance config display 2024-09-09 08:31:20 +03:00
ebc5cafb2b protection 2024-09-08 17:46:21 +03:00
Tal
52e8d7bc6a Merge pull request #1207 from Codium-ai/tr/fixes_metadata
Tr/fixes metadata
2024-09-08 17:32:58 +03:00
f7344fd787 metadata.md docs 2024-09-08 17:31:17 +03:00
86103c65e8 pattern_back 2024-09-08 17:24:13 +03:00
a4658b9960 docs 2024-09-08 16:52:20 +03:00
5fd831c448 impact_evaluation 2024-09-08 16:47:04 +03:00
332d3a0c5e markdown update 2024-09-08 16:43:44 +03:00
Tal
edef712b6a Merge pull request #1206 from Codium-ai/tr/ai_metadata
ai_metadat_injection
2024-09-08 16:33:00 +03:00
1831f2cec4 markdown 2024-09-08 16:32:46 +03:00
8706f643ef enable ai_metadata 2024-09-08 16:26:26 +03:00
Tal
35a75095ea Merge pull request #1205 from ScArLeXiA/fix-typo-in-readme
fix: Typo in README (`Chrom` -> `Chrome`)
2024-09-07 19:20:35 +03:00
0aa296d03e fix: Typo in README (Chrom -> Chrome) 2024-09-08 01:14:41 +09:00
Tal
24f7e8622f Merge pull request #1202 from Codium-ai/tr/ignore_titile_adjustments
avoid_full_files
2024-09-07 11:49:21 +03:00
d01cfe443c avoid_full_files 2024-09-07 11:44:24 +03:00
Tal
6150256040 Merge pull request #1201 from Codium-ai/tr/ignore_titile_adjustments
Tr/ignore titile adjustments
2024-09-07 11:27:28 +03:00
147a8e0ef3 refactor: consolidate PR ignore logic into a single function and update documentation 2024-09-07 11:26:13 +03:00
Tal
9199d84796 Merge pull request #1192 from paolomainardi/feature/1190_exclude_branches_tags
feat: gitlab skip source, target and labels
2024-09-07 10:20:31 +03:00
39913ef12a fix: remove specific configurations 2024-09-06 20:23:33 +02:00
d2a744e70c fix: remove line 2024-09-06 18:42:47 +02:00
be93c52380 fix: remove line 2024-09-06 18:42:28 +02:00
7ccefca35e fix: remove comment 2024-09-06 18:41:36 +02:00
14b4723734 feat: move configuration to a common config section, add documentation 2024-09-06 18:32:46 +02:00
c8f1c03061 fix: correct tuple unpacking in GitHub polling task queue loop 2024-09-05 20:31:17 +03:00
Tal
b02fa22948 Merge pull request #1198 from Codium-ai/tr/polling
Tr/polling
2024-09-05 19:58:51 +03:00
85754d2d79 feat: enhance GitHub polling with synchronous comment processing and improved logging 2024-09-05 16:57:10 +03:00
f0d780c7ec feat: enhance GitHub polling with synchronous comment processing and improved logging 2024-09-05 16:55:10 +03:00
19048ee705 feat: enhance GitHub polling with synchronous comment processing and improved logging 2024-09-05 16:53:31 +03:00
b8d2b263b9 feat: enhance GitHub polling with synchronous comment processing and improved logging and bug fixing 2024-09-05 16:52:47 +03:00
Tal
6f17c08f72 Merge pull request #1197 from proDOOMman/onec_enterprice
Add 1C Enterprise language
2024-09-04 15:20:16 +03:00
65c0bc414f docs: add supported browsers section to Chrome extension documentation 2024-09-04 13:36:44 +03:00
015719134f docs: enhance PR chat section with context-aware explanation in features.md 2024-09-04 12:01:23 +03:00
1ed6b7a54a docs: enhance PR chat section with context-aware explanation in features.md 2024-09-04 11:57:24 +03:00
14067a02db docs: update Chrome extension documentation with installation video link and image update 2024-09-04 11:01:04 +03:00
be75bb6a16 Add 1C Enterprise language 2024-09-04 09:04:39 +03:00
883d945687 Merge remote-tracking branch 'origin/main' 2024-09-04 08:52:32 +03:00
8090115f30 docs: update Chrome extension description and bump version to 0.2.4 2024-09-04 08:52:22 +03:00
Tal
6fa226dee7 Merge pull request #1196 from Codium-ai/mrT23-patch-11
Update README.md
2024-09-03 18:50:47 +03:00
Tal
13c1cdbf90 Update README.md 2024-09-03 18:49:04 +03:00
Tal
d4d9a7f8b4 Merge pull request #1172 from MaxHoecker/bugfix/bb-2-way-diff-fix
Bugfix/bb 2 way diff fix
2024-09-03 16:24:42 +03:00
c14c49727f docs: restructure Chrome extension documentation and add features and data privacy sections 2024-09-02 21:25:54 +03:00
292a5015d6 docs: restructure Chrome extension documentation and add features and data privacy sections 2024-09-02 21:16:10 +03:00
2f7f60a469 fix: review standardize regex checking 2024-09-02 16:31:19 +02:00
adce35765b feat: implement skip branches for github, add ignore title to gitlab 2024-09-02 16:26:50 +02:00
6776f7c296 Merge remote-tracking branch 'origin/main' 2024-09-02 08:28:10 +03:00
7287a94e88 docs: update installation guide with corrected links for language models and PR-Agent Pro details 2024-09-02 08:27:58 +03:00
Tal
e2cf1d0068 Update README.md 2024-09-01 11:17:41 +03:00
8ada3111ec docs: enhance PR-Agent Pro overview and update image sizes in tools documentation 2024-09-01 11:02:10 +03:00
Tal
9c9611e81a Merge pull request #1195 from Codium-ai/tr/updates
docs: update PR-Agent Pro documentation with new features and improve…
2024-09-01 10:54:16 +03:00
Tal
4fb93e3b62 Update docs/docs/overview/pr_agent_pro.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-09-01 10:53:48 +03:00
5a27e1dd7e docs: update PR-Agent Pro documentation with new features and improvements 2024-09-01 10:51:29 +03:00
Tal
6e6151d201 Merge pull request #1194 from Codium-ai/tr/updates
docs: update PR-Agent Pro documentation with new features and improve…
2024-09-01 10:38:31 +03:00
e468efb53e docs: update PR-Agent Pro documentation with new features and improvements 2024-09-01 10:38:06 +03:00
Tal
95e1ebada1 Update docs/docs/tools/improve.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-09-01 10:37:40 +03:00
d74c867eca docs: update PR-Agent Pro documentation with new features and improvements
- Added new features section with detailed descriptions and links
- Updated the overview of the `improve` tool with new functionalities and images
- Corrected image links in README.md for PR Chat feature
2024-09-01 10:35:17 +03:00
Tal
2448281a45 Merge pull request #1193 from Codium-ai/tr/updates
Tr/updates
2024-09-01 08:49:16 +03:00
9e063bf48a fix: handle missing score in code suggestions and adjust indentation in pr_code_suggestions.py 2024-09-01 08:47:23 +03:00
5432469ef6 fix: ensure non-empty lines are processed correctly in git patch handling 2024-09-01 08:39:29 +03:00
2c496b9d4e fix: improve PR patch decoding and clean up Bitbucket provider code
- Add fallback encodings for PR patch decoding to handle non-UTF-8 encodings.
- Update logging messages for better clarity.
- Remove unnecessary blank lines and fix minor formatting issues.
- Ensure full files are retrieved in `get_diff_files` method.
2024-09-01 08:38:26 +03:00
Tal
5ac41dddd6 Merge pull request #1191 from AHancher2022/bugfix/fix-output-publishing-when-set-to-false
Fixing code suggestions being published when publish ouput is false
2024-08-31 16:35:09 +03:00
Tal
9df554ed1c Merge pull request #1185 from jozefvodicka/patch-1
Update pr_update_changelog.py to include "[skip ci]" so pipeline is n…
2024-08-31 16:33:18 +03:00
23af1afa03 feat: gitlab skip source, target and labels 2024-08-30 17:01:18 +02:00
fdcbdfce98 feat: gitlab skip source, target and labels 2024-08-30 16:40:23 +02:00
cf14e45674 further cleaned up code based on feedback 2024-08-30 10:10:39 -04:00
=
1c51b5b762 Fixing code suggestions being published if there are no code suggestions when publish output is false 2024-08-30 09:12:22 -04:00
Tal
e5715e12cb Merge pull request #1189 from woung717/fix/litellm-timeout
fix: change deprecated timeout parameter for litellm
2024-08-29 17:01:24 +03:00
578d7c69f8 fix: change deprecated timeout parameter for litellm 2024-08-29 21:45:48 +09:00
29c50758bc implementing more feedback, choosing a different Bitbucket diff strategy depending on API version, and expanded unit test cases 2024-08-28 17:13:36 -04:00
Tal
97b48da03b Merge pull request #1184 from MaxHoecker/bugfix/suggestions_error_publish
checking for publish_output in the suggestions tool exception handler
2024-08-28 20:35:05 +03:00
4203ee4ca8 Update pr_update_changelog.py to include "[skip ci]" so pipeline is not triggered 2024-08-28 12:08:39 +02:00
Tal
84dc976ebb Merge pull request #1163 from MarkRx/bugfix/asyncio-task-completion
Ensure asyncio event queue is drained prior to CLI termination
2024-08-28 07:54:37 +03:00
d9571ee7cb checking for publish_output in the suggestions tool exception handler 2024-08-28 00:41:56 -04:00
Tal
7373ed36e6 Update data_privacy.md 2024-08-27 17:18:45 +03:00
Tal
cdf13925b0 Merge pull request #1179 from Codium-ai/tr/patch_fixes
fix: handle deleted files in git patch processing and update section header logic
2024-08-27 09:37:35 +03:00
c2f52539aa fix: handle deleted files in git patch processing and update section header logic 2024-08-27 09:31:31 +03:00
0442cdcd3d adding config value for old Bitbucket Server diff functionality 2024-08-26 16:07:21 -04:00
93773f3c08 Ensure asyncio event queue is drained prior to CLI termination 2024-08-26 15:14:45 -04:00
Tal
53a974c282 Merge pull request #1177 from paolomainardi/patch-2
feat: keep manual comments working when commenting draft MRs
2024-08-26 17:39:24 +03:00
c9ed271eaf feat: keep manual comments working when commenting draft MRs
closes #1160
2024-08-26 14:50:43 +02:00
Tal
6a5ff2fa3b Update README.md 2024-08-26 08:27:58 +03:00
Tal
25d661c152 Update README.md 2024-08-26 08:00:07 +03:00
Tal
d20c9c6c94 Merge pull request #1175 from Codium-ai/tr/protections2
Tr/protections2
2024-08-25 12:16:57 +03:00
d1d861e163 update tests 2024-08-25 12:11:16 +03:00
033db1015e update tests 2024-08-25 12:08:02 +03:00
abf2f68c61 fix: update description for pr_files field in PRDescription class in pr_description_prompts.toml 2024-08-25 12:02:14 +03:00
441e098e2a fix: correct YAML formatting in response text processing in utils.py 2024-08-25 11:26:48 +03:00
Tal
2bbf4b366e Merge pull request #1174 from Codium-ai/tr/protections2
Tr/protections2
2024-08-25 08:13:29 +03:00
b9d096187a fix: skip processing comments without a body in GitHub polling server 2024-08-25 07:21:40 +03:00
ce156751e8 Merge remote-tracking branch 'origin/main' into tr/protections2
# Conflicts:
#	pr_agent/servers/github_polling.py
2024-08-25 07:19:38 +03:00
Tal
dae87d7da8 Merge pull request #1170 from Codium-ai/tr/avoid_repeating_pr_changes
Update PR code suggestions prompts
2024-08-25 07:17:47 +03:00
a99ebf8953 implementing PR bot feedback 2024-08-23 11:18:42 -04:00
2a9e3ee1ef removing unnecessary if check 2024-08-23 10:57:58 -04:00
2beefab89a BB server 2 way diff fixes 2024-08-23 10:47:27 -04:00
415f44d763 type 2024-08-23 11:27:50 +03:00
8fb9b8ed3e Update PR code suggestions prompts to avoid repeating changes already present in the PR 2024-08-23 11:22:55 +03:00
Tal
4f1dccf67b Merge pull request #1165 from paolomainardi/feature/1160_gitlab_mr_draft_skip
feat: handle gitlab MR draft status
2024-08-23 10:43:00 +03:00
3778cc2745 feat: skip draft by default 2024-08-22 21:59:01 +02:00
8793f8d9b0 Update gitlab_webhook.py 2024-08-22 21:14:49 +02:00
61837c69a3 Update gitlab_webhook.py 2024-08-22 21:13:54 +02:00
ffaf5d5271 feat: Handle the gitlab MR draft status
closes #1160
2024-08-22 17:29:39 +02:00
Tal
cd526a233c Update additional_configurations.md 2024-08-22 11:26:38 +03:00
Tal
745e955d1f Merge pull request #1145 from MarkRx/feature/litellm-logging-observability
Add and document abilty to use LiteLLM Logging Observability tools
2024-08-22 09:58:53 +03:00
Tal
771d0b8c60 Update github.md 2024-08-22 07:51:09 +03:00
Tal
91a7c08546 Merge pull request #1161 from paolomainardi/patch-1
fix: remove CI_MERGE_REQUEST_STATE as is not a Gitlab variable
2024-08-21 18:41:19 +03:00
4d9d6f7477 fix: remove CI_MERGE_REQUEST_STATE as is not a Gitlab variable 2024-08-21 11:36:33 +02:00
2591a5d6c1 patch_extension_skip_types
Add validation for latest_comment and cast suggestions_score_threshold to int
2024-08-20 12:11:34 +03:00
Tal
772499fce1 Merge pull request #1159 from Codium-ai/tr/patches
Tr/patches
2024-08-20 11:41:05 +03:00
d467f5a7fd patch_extension_skip_types 2024-08-20 11:37:27 +03:00
2d5b060168 patch_extension_skip_types 2024-08-20 11:33:56 +03:00
b7eb6be5a0 Update PR code suggestions and reviewer prompts for clarity and consistency 2024-08-20 11:27:35 +03:00
df57367426 Update configuration.toml to modify fallback models and add skip types for patch extension logic 2024-08-20 11:24:52 +03:00
660a60924e Add filename parameter and skip logic to extend_patch function in git_patch_processing.py 2024-08-20 11:23:37 +03:00
8aa76a0ac5 Add and document abilty to use LiteLLM Logging Observability tools 2024-08-19 15:45:47 -04:00
b034d16c23 Add validation checks for 'latest_comment' and 'pr_url' in GitHub polling server 2024-08-19 14:18:45 +03:00
Tal
9bec97c66c Merge pull request #1156 from Codium-ai/tr/dynamic_context
Tr/dynamic context
2024-08-19 14:01:42 +03:00
8fd8d298e7 Add unit tests for dynamic context handling in extend_patch function 2024-08-19 10:33:47 +03:00
Tal
2e186ebae8 Merge pull request #1154 from KennyDizi/main
Enhance variable consistency in prompt rendering for PR tools
2024-08-19 08:48:10 +03:00
fc40ca9196 Refactor dynamic context handling in git patch processing and update configuration default 2024-08-19 08:38:26 +03:00
91a8938a37 update links 2024-08-19 07:57:52 +03:00
d97e1862da Add trial information and target audience details to PR-Agent Pro documentation 2024-08-19 07:50:26 +03:00
Tal
f042c061de Merge pull request #1155 from Codium-ai/add_installation_details
Update index.md
2024-08-19 07:42:52 +03:00
Tal
c47afd9c0d Update docs/docs/chrome-extension/index.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-08-19 07:42:23 +03:00
c6d16ced07 Update index.md 2024-08-19 00:28:25 +03:00
e9535ea164 Add dynamic context handling in git patch processing
- Introduce `allow_dynamic_context` and `max_extra_lines_before_dynamic_context` settings.
- Adjust context limits dynamically based on section headers.
- Add logging for dynamic context adjustments and section header findings.
2024-08-18 17:45:18 +03:00
dc8a4be2d4 Add new line for a better visually 2024-08-18 20:38:00 +07:00
f9de8f283b Improve variable consistency in prompt rendering for pr generate labels flow 2024-08-18 20:30:10 +07:00
bd5c19ee05 Improve variable consistency in prompt rendering for pr description flow 2024-08-18 20:29:59 +07:00
Tal
7cbe797108 Update review.md 2024-08-18 09:16:46 +03:00
Tal
435d9d41c8 Update README.md 2024-08-18 08:44:49 +03:00
Tal
a510d93e6e Merge pull request #1153 from Codium-ai/tr/protections2
Tr/protections2
2024-08-18 08:34:19 +03:00
48cc2f6833 Limit comment body length and adjust logging levels in GitHub provider 2024-08-18 08:26:01 +03:00
229d7b34c7 Limit comment body length and adjust logging levels in GitHub provider 2024-08-18 08:24:27 +03:00
03b194c337 Add null and type checks for 'pr_files' in _prepare_file_labels method in pr_description.py 2024-08-18 08:21:32 +03:00
a6f772c6d5 Refactor comment deletion method to 'remove_comment' in git providers and update references 2024-08-18 08:19:25 +03:00
ba1ba98dec Add logging for empty PR files and code suggestions status in pr_code_suggestions.py 2024-08-18 08:17:25 +03:00
5954c7cec2 Add logging for empty PR files and update warning for empty diffs in pr_reviewer.py 2024-08-18 08:14:37 +03:00
dc1a8e8314 Add detailed logging and error handling in GitHub polling server
- Introduce traceback logging for exceptions during notification processing.
- Enhance logging for PR comments with additional artifact information.
- Update configuration settings for publishing PR descriptions as comments.
2024-08-18 08:13:40 +03:00
aa87bc60f6 Rename 'add_callbacks' to 'add_litellm_callbacks' for clarity in litellm_ai_handler 2024-08-17 09:20:30 +03:00
c76aabc71e Add callback functionality to litellm_ai_handler for enhanced logging and metadata capture 2024-08-17 09:15:05 +03:00
Tal
81081186d9 Merge pull request #1151 from Codium-ai/publish_inline
Add 'original_suggestion' parameter to publish_inline_comment methods…
2024-08-16 09:08:05 +03:00
4a71ec90c6 Add null check for 'suggestion' in publish_code_suggestions method in gitlab_provider.py 2024-08-16 09:04:45 +03:00
3456c8e039 Add 'original_suggestion' parameter to publish_inline_comment methods across git providers for enhanced inline comment handling 2024-08-16 08:58:51 +03:00
Tal
402a388be0 Merge pull request #1126 from R-Mathis/feat/upgrade_libraries
update litellm library
2024-08-15 12:14:30 +03:00
4e26c02b01 update litellm lib 2024-08-15 08:25:51 +02:00
Tal
ea4f88edd3 Merge pull request #1142 from Codium-ai/tr/docs_chat
Add security and privacy section to Chrome extension documentation
2024-08-14 17:33:05 +03:00
217f615dfb Add security and privacy section to Chrome extension documentation 2024-08-14 17:32:23 +03:00
Tal
a6fb351789 Merge pull request #1141 from Codium-ai/tr/docs_chat
Add documentation for PR-Chat feature in PR-Agent Chrome extension
2024-08-14 17:29:17 +03:00
bfab660414 Add security and privacy section to Chrome extension documentation 2024-08-14 17:28:12 +03:00
2e63653bb0 Add documentation for PR-Chat feature in PR-Agent Chrome extension 2024-08-14 17:22:16 +03:00
Tal
b9df034c97 Merge pull request #1138 from Codium-ai/tr/err_protections
Add 'only_markdown' parameter to emphasize_header call in utils.py fo…
2024-08-14 14:03:43 +03:00
bae8d36698 Add 'only_markdown' parameter to emphasize_header call in utils.py for security concerns section 2024-08-14 14:02:09 +03:00
Tal
67a04e1cb2 Merge pull request #1135 from Codium-ai/hl/fix_code_escaping
fix html escaping
2024-08-14 12:32:52 +03:00
4fea780b9b fix html escaping 2024-08-14 12:13:51 +03:00
Tal
01c18d7d98 Merge pull request #1134 from Codium-ai/tr/err_protections
Tr/err protections
2024-08-14 08:17:19 +03:00
f4b06640d2 Add info log for successful AI prediction parse in utils.py 2024-08-14 08:14:51 +03:00
f1981092d3 Add warning log for initial AI prediction parse failure and error log for fallback failure in utils.py 2024-08-14 08:08:55 +03:00
8414e109c5 Update logging levels and add error handling for empty data in PR tools and providers 2024-08-14 08:02:34 +03:00
8adfca5b3c Add error handling for short diffs and improve logging in Bitbucket provider 2024-08-14 07:54:11 +03:00
Tal
672cdc03ab Merge pull request #1133 from Codium-ai/tr/err_protections
Add error handling for empty diff files in utils.py and optimize file…
2024-08-13 22:36:00 +03:00
86a9cfedc8 Add error handling for empty diff files in utils.py and optimize file content retrieval in Bitbucket provider 2024-08-13 22:33:07 +03:00
Tal
7ac9f27b70 Merge pull request #1132 from Codium-ai/tr/err_protections
Add error handling for missing GitLab URL and improve inline comment …
2024-08-13 19:05:00 +03:00
Tal
c97c39d57d Merge pull request #1127 from squinn1/main
Fix base url not being passed through github_provider class correctly
2024-08-13 19:03:12 +03:00
a3b3d6c77a Add error handling for missing GitLab URL and improve inline comment logic in gitlab_provider.py 2024-08-13 19:00:09 +03:00
2e41701d07 fixes 2024-08-13 16:21:46 +01:00
578f56148a Merge branch 'Codium-ai:main' into main 2024-08-13 16:04:55 +01:00
Tal
b3da84b4aa Merge pull request #1130 from Codium-ai/tr/err_protections
Tr/err protections
2024-08-13 17:03:20 +03:00
f89bdcf3c3 Add error handling for missing custom label settings in utils.py 2024-08-13 16:40:05 +03:00
e7e3970874 Add error handling for empty system prompt in litellm_ai_handler and type conversion in utils.py 2024-08-13 16:26:32 +03:00
Tal
1f7a8eada0 Merge pull request #1129 from Codium-ai/tr/err_protections
Tr/err protections
2024-08-13 13:08:21 +03:00
38638bd1c4 relevant_lines_start > len(file_lines): 2024-08-13 12:59:51 +03:00
26f3bd8900 Add error handling for out-of-range relevant_lines_start and missing head_file in pr_code_suggestions.py 2024-08-13 12:57:31 +03:00
a2fb415c53 Add git_files attribute to Bitbucket provider class for enhanced file handling 2024-08-13 12:39:45 +03:00
8038eaf876 Add error handling for missing required fields in file label dictionary in pr_description.py 2024-08-13 12:16:52 +03:00
Tal
d8572f8d13 Merge pull request #1128 from Codium-ai/tr/err_protections
Tr/err_protections
2024-08-13 11:58:09 +03:00
78b11c80c7 Add error handling for empty secrets in GitLab webhook and lower log level for Google Cloud Storage secret retrieval errors 2024-08-13 11:42:07 +03:00
cb65b05e85 Add error handling for missing username fields in Bitbucket webhook handler and update log context 2024-08-13 11:33:19 +03:00
1aa6dd9b5d Add error handling for missing file paths in Bitbucket provider and improve file validation logic 2024-08-13 11:28:21 +03:00
11d69e05aa fix git provider 2024-08-13 09:25:59 +01:00
0722af4702 update openai lib 2024-08-13 09:53:11 +02:00
99e99345b2 update litellm library 2024-08-13 09:44:08 +02:00
5252e1826d Add handling for empty diffs in Bitbucket provider to avoid logging errors 2024-08-13 09:45:45 +03:00
Tal
a18a0bf2e3 Merge pull request #1125 from Codium-ai/tr/err_protections
Tr/err protections
2024-08-13 09:03:25 +03:00
396d11aa45 Improve logging and diff filtering in Bitbucket provider
- Enhance logging to include both kept and filtered file names
- Add conditional check to filter diff elements based on original and current diffs
2024-08-13 08:59:45 +03:00
4a38861d06 Add error handling for missing file paths in file_filter.py for Bitbucket and GitLab platforms 2024-08-13 08:59:27 +03:00
Tal
5feb66597e Merge pull request #1124 from Codium-ai/tr/err_protections
protections
2024-08-12 21:25:43 +03:00
8589941ffe Improve Bitbucket diff handling for new and deleted files and add error protection for missing file links 2024-08-12 21:15:26 +03:00
7f0e6aeb37 Add conditional checks for review label settings in set_review_labels method 2024-08-12 20:57:32 +03:00
8a768aa7fd Lower log level for missing code suggestions from error to warning 2024-08-12 18:41:24 +03:00
Tal
f399f9ebe4 Update automations_and_usage.md 2024-08-12 17:28:28 +03:00
Tal
cc73d4599b Merge pull request #1123 from Codium-ai/ok/app_name
Add app_name to log_context in Bitbucket and GitLab webhook handlers
2024-08-12 16:26:08 +03:00
Tal
4228f92e7e Merge pull request #1119 from Codium-ai/hl/limit_long_comments
Hl/limit long comments
2024-08-12 16:25:42 +03:00
Tal
1f4ab43fa6 Merge pull request #1118 from Codium-ai/tr/bitbucket_diffs
Improve Bitbucket patch diff handling
2024-08-12 16:24:28 +03:00
b59111e4a6 Add app_name to log_context in Bitbucket and GitLab webhook handlers 2024-08-12 16:10:41 +03:00
70da871876 lower OpenAI errors to warnings 2024-08-12 12:27:48 +03:00
9c1ab06491 fix long comments 2024-08-12 12:27:09 +03:00
5c4bc0a008 Add Bitbucket diff handling and improve error logging
- Implement `publish_file_comments` method placeholder
- Enhance `is_supported` method to include `publish_file_comments`
- Refactor diff splitting logic to handle Bitbucket-specific headers
- Improve error handling and logging for file content retrieval
- Add `get_pr_owner_id` method to retrieve PR owner ID
- Update `_get_pr_file_content` to fetch file content from remote link
- Fix variable name typo in `extend_patch` function in `git_patch_processing.py`
2024-08-12 09:48:26 +03:00
Tal
ef37271ce9 Update README.md 2024-08-11 19:16:11 +03:00
Tal
8dd4c15d4b Merge pull request #1117 from Codium-ai/tr/patch_extra_lines_before_and_after
Add missing newline in extended patch and remove trailing whitespace
2024-08-11 19:04:54 +03:00
f9afada1ed tests 2024-08-11 19:01:42 +03:00
4c1c313031 Add missing newline in extended patch and remove trailing whitespace 2024-08-11 18:49:28 +03:00
Tal
1f126069b1 Merge pull request #1116 from Codium-ai/tr/patch_extra_lines_before_and_after
Tr/patch extra lines before and after
2024-08-11 15:57:12 +03:00
12742ef499 Adjust patch extension logic to handle cases where extended size exceeds original file length 2024-08-11 15:48:58 +03:00
63e921a2c5 Adjust patch extension logic to handle cases where extended size exceeds original file length 2024-08-11 15:46:46 +03:00
Tal
8f04387331 Merge pull request #1115 from Codium-ai/tr/patch_extra_lines_before_and_after
Fix incorrect logic for extending patch size beyond original file length
2024-08-11 15:37:44 +03:00
a06670bc27 Fix incorrect logic for extending patch size beyond original file length 2024-08-11 15:20:27 +03:00
Tal
2525392814 Merge pull request #1114 from Codium-ai/tr/patch_extra_lines_before_and_after
Tr/patch extra lines before and after
2024-08-11 14:04:57 +03:00
23aa2a9388 Refactor patch extension logic to handle cases with zero extra lines 2024-08-11 13:59:27 +03:00
e85b75fe64 Refactor patch extension logic to handle cases with zero extra lines 2024-08-11 12:56:56 +03:00
df04a7e046 Add spaces to extra lines in patch extension for consistency 2024-08-11 12:32:26 +03:00
9c3f080112 comments 2024-08-11 12:15:47 +03:00
ed65493718 Handle edge cases for patch extension and update tests 2024-08-11 12:08:00 +03:00
983233c193 Clarify comments for patch_extra_lines_before and patch_extra_lines_after in configuration.toml 2024-08-11 11:48:50 +03:00
7438190ed1 set_claude_model 2024-08-11 11:43:57 +03:00
2b2b851cb9 Update test class name and adjust patch extra lines configuration
- Renamed test class to `TestExtendedPatchMoreLines` in `test_extend_patch.py`
- Imported `pr_generate_extended_diff` in `test_extend_patch.py`
- Updated `patch_extra_lines_before` to 4 in `additional_configurations.md`
2024-08-11 11:29:31 +03:00
5701816b2e Merge remote-tracking branch 'origin/main' into tr/patch_extra_lines_before_and_after 2024-08-11 11:26:53 +03:00
Tal
40a25a1082 Create codecov.yml 2024-08-11 09:55:00 +03:00
e238a88824 Add tests for patch extension and update configuration for extra lines handling
- Added unit tests in `test_extend_patch.py` and `test_pr_generate_extended_diff.py` to verify patch extension functionality with extra lines.
- Updated `pr_processing.py` to include `patch_extra_lines_before` and `patch_extra_lines_after` settings.
- Modified `configuration.toml` to adjust `patch_extra_lines_before` to 4 and `max_context_tokens` to 16000.
- Enabled extra lines in `pr_code_suggestions.py`.
- Added new model `claude-3-5-sonnet` to `__init__.py`.
2024-08-11 09:21:34 +03:00
61bdfd3b99 patch_extra_lines_before and patch_extra_lines_after 2024-08-10 21:55:51 +03:00
Tal
c00d1e9858 Update README.md 2024-08-10 21:43:37 +03:00
Tal
1a8b143f58 Update README.md 2024-08-10 21:36:27 +03:00
Tal
dfbe7432b8 Merge pull request #1108 from Codium-ai/tr/azure_pipeline
Tr/azure pipeline
2024-08-10 21:35:12 +03:00
ab69f1769b azure 2024-08-10 21:31:17 +03:00
089210d9fa azure 2024-08-10 21:30:18 +03:00
Tal
0f9d89c67a Merge pull request #1107 from Codium-ai/tr/protections
protections
2024-08-10 20:57:13 +03:00
84b80f792d protections 2024-08-09 21:44:00 +03:00
Tal
219d962cbe Merge pull request #1106 from Codium-ai/tr/protections
protections
2024-08-09 21:29:24 +03:00
e531245f4a protections 2024-08-09 21:15:29 +03:00
89e9413d75 logger message 2024-08-09 20:21:00 +03:00
Tal
b370cb6ae7 Merge pull request #1102 from MarkRx/feature/langchain-azure-fix
Fix LangChainOpenAIHandler for Azure
2024-08-08 19:37:26 +03:00
4201779ce2 Fix LangChainOpenAIHandler for Azure 2024-08-08 09:55:18 -04:00
Tal
71f7c09ed7 Merge pull request #1100 from ascieux-soprasteria/main
Clarification on webhook URL for Gitlab
2024-08-08 11:02:38 +03:00
edad244a86 Update gitlab.md
Changing 4 backticks to 3 backticks
2024-08-08 09:11:27 +02:00
9752987966 Merge branch 'Codium-ai:main' into main 2024-08-08 09:10:16 +02:00
Tal
200da44e5a Merge pull request #1103 from benedict-lee/main
Fix pr_processing.get_pr_multi_diffs
2024-08-08 07:55:08 +03:00
4c0fd37ac2 Fix pr_processing.get_pr_multi_diffs
Fix function to return an empty list instead of a single joined string when patches_extended is empty.
2024-08-08 11:46:26 +09:00
c996c7117f Fix function to return an empty list instead of a single joined string when patches_extended is empty. 2024-08-08 11:32:10 +09:00
943ba95924 Clarification on webhook URL for Gitlab 2024-08-07 16:00:12 +02:00
8a75d3101d Enhance PR description handling and improve logging for invalid file extensions in Bitbucket provider 2024-08-07 16:32:36 +03:00
Tal
944f54b431 Merge pull request #1098 from KennyDizi/main
Add support for GPT-4o-2024-08-06 model
2024-08-07 08:44:28 +03:00
9be5cc6dec Add support model gpt-4o-2024-08-06 2024-08-07 07:28:51 +07:00
Tal
884286ebf1 Merge pull request #1095 from Codium-ai/tr/code_cov
Tr/code cov
2024-08-06 19:57:37 +03:00
Tal
620dbbeb1a Update .github/workflows/code_coverage.yaml
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-08-06 19:45:35 +03:00
Tal
c07059e139 Update .github/workflows/code_coverage.yaml
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-08-06 19:45:14 +03:00
Tal
3b88d6afdb Update .pr_agent.toml 2024-08-06 19:42:11 +03:00
e717e8ae81 Update code_coverage.yaml to improve Docker test execution and Codecov upload 2024-08-06 19:38:59 +03:00
Tal
8ec1fb5937 Merge pull request #1093 from Codium-ai/tr/html_escaping
Escape HTML tags in suggestion summaries in pr_code_suggestions.py
2024-08-06 12:19:54 +03:00
cb10ceadd7 Escape HTML tags in suggestion summaries in pr_code_suggestions.py and update pr_code_suggestions_prompts.toml for backtick usage 2024-08-06 12:16:58 +03:00
96d3f3cc0b s 2024-08-06 11:20:37 +03:00
Tal
a98d972041 Update PR_agent_pro_models.md 2024-08-06 08:02:07 +03:00
09a1d74a00 Add instructions for running PR-Agent as a GitLab pipeline in gitlab.md 2024-08-05 15:57:09 +03:00
Tal
31f6f8f8ea Merge pull request #1092 from Codium-ai/tr/gitlab_pipeline
Tr/gitlab pipeline
2024-08-05 15:54:11 +03:00
e7c99f0e6f Add instructions for running PR-Agent as a GitLab pipeline in gitlab.md 2024-08-05 15:52:13 +03:00
ac53e6728d Add instructions for running PR-Agent as a GitLab pipeline in gitlab.md 2024-08-05 15:48:42 +03:00
b100e7098a Add instructions for running PR-Agent as a GitLab pipeline in gitlab.md 2024-08-05 15:45:58 +03:00
Tal
2b77d07725 Merge pull request #1091 from Codium-ai/tr/patch_improvements
patch and prompt improvements
2024-08-04 14:23:45 +03:00
ee1676cf7e patch improvements 2024-08-03 12:49:58 +03:00
3420e6f30d patch improvements 2024-08-03 12:44:49 +03:00
Tal
85cc0ad08c Merge pull request #1087 from KennyDizi/main
Fix exception handling and remove unused import in Bitbucket server provider
2024-08-03 12:18:12 +03:00
Tal
3756b547da Merge pull request #1090 from Codium-ai/mrT23-patch-10
Update test_convert_to_markdown.py
2024-08-02 21:54:51 +03:00
e34bcace29 s 2024-08-02 21:51:54 +03:00
Tal
2a675b80ca Update test_convert_to_markdown.py 2024-08-02 21:43:11 +03:00
Tal
1cefd23739 Merge pull request #1073 from h0rv/patch-1
Improve response cleaning
2024-08-02 12:21:40 +03:00
aef9a04b32 Fix use requests HTTPError 2024-08-02 07:58:23 +07:00
fe4e642a47 Removed unused json import 2024-08-02 07:57:09 +07:00
039d85b836 fix cleaning 2024-08-01 15:50:00 -04:00
0fa342ddd2 pr_description.final_update_message=false 2024-08-01 09:51:35 +03:00
Tal
c95a8cde72 Merge pull request #1083 from pc-bob/BL/fix-security-vulns
update dependencies to fix various high/critical security vulnerabilies
2024-08-01 09:35:13 +03:00
Tal
23ec25c949 Merge pull request #1079 from MarkRx/feature/bitbucket-server-multi-line-suggestion-workaround
Use code blocks instead of suggestion blocks for Bitbucket Server multi-line suggestions
2024-08-01 09:29:18 +03:00
Tal
9560bc1b44 Merge pull request #1081 from MarkRx/feature/bitbucket-server-fix-post-inline-comments
Bitbucket Server fix inline comments and reading .pr_agent.toml
2024-08-01 09:28:35 +03:00
346ea8fbae update dependencies to fix various high/critical security vulnerabilities 2024-07-31 15:04:43 -07:00
d671c78233 Merge remote-tracking branch 'origin/main' 2024-07-31 13:32:51 +03:00
240e0374e7 fixed extra call bug 2024-07-31 13:32:42 +03:00
288e9bb8ca Fix Bitbucket Server 401 unauthorized when posting inline coments; fix Bitbucket loading .pr_agent.toml 2024-07-30 16:19:57 -04:00
Tal
d8545a2b28 Merge pull request #1075 from MarkRx/feature/bitbucket-server-pr-commands-defaults
Add missing bitbucket_server.pr_commands default
2024-07-30 18:03:19 +03:00
95f23de7ec Use code blocks instead of suggestion blocks for Bitbucket Server multi-line suggestions to workaround BSERV-4553 2024-07-30 10:43:56 -04:00
0390a85f5a Add missing bitbucket_server.pr_commands default 2024-07-30 09:45:45 -04:00
172d0c0358 improve response cleaning
The prompt for the model starts with a code block (```). When testing watsonx models (llama and granite), they would generate the closing block in the response.
2024-07-29 10:26:58 -04:00
Tal
41588efe9a Merge pull request #1069 from Codium-ai/watson_models
watson_models
2024-07-29 08:39:37 +03:00
Tal
f50832e19b Update __init__.py 2024-07-29 08:32:34 +03:00
Tal
927f124dca Update configuration_options.md 2024-07-28 18:12:22 +03:00
Tal
232b540f60 Update README.md 2024-07-28 17:34:08 +03:00
Tal
452eda25cd Update changing_a_model.md 2024-07-28 09:57:23 +03:00
Tal
110e593d03 Update README.md 2024-07-28 09:36:41 +03:00
Tal
af84409c1d Merge pull request #1067 from Codium-ai/tr/custom_model
docs: update usage guide and README; fix minor formatting issues in u…
2024-07-28 09:34:05 +03:00
c2c69f2950 No code suggestions found for PR. 2024-07-28 09:32:54 +03:00
e946a0ea9f docs: update usage guide and README; fix minor formatting issues in utils.py 2024-07-28 09:30:21 +03:00
Tal
866476080c Merge pull request #1066 from Codium-ai/tr/custom_model
docs: update usage guide for changing models; add custom model support and reorganize sections
2024-07-28 09:00:48 +03:00
Tal
27d6560de8 Update pr_agent/algo/utils.py
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-28 08:58:03 +03:00
6ba7b3eea2 fix condition 2024-07-28 08:57:39 +03:00
86d9612882 docs: update usage guide for changing models; add custom model support and reorganize sections 2024-07-28 08:55:01 +03:00
Tal
49f608c968 Merge pull request #1065 from dceoy/feature/add-groq-models
Add Llama 3.1 and Mixtral 8x7B for Groq
2024-07-28 08:31:50 +03:00
Tal
11f85cad62 Merge pull request #1064 from KennyDizi/main
Fix Bitbucket token name and clean up secrets template
2024-07-28 08:29:17 +03:00
Tal
5f5257d195 Merge pull request #1063 from Codium-ai/tr/seed_and_tests
Add end-to-end tests for GitHub, GitLab, and Bitbucket apps; added seed
2024-07-28 08:27:07 +03:00
495e2ccb7d Add Llama 3.1 and Mixtral 8x7B for Groq 2024-07-28 02:28:42 +09:00
a176adb23e Remove redundant spaces 2024-07-27 22:21:54 +07:00
68ef11a2fc Fix invalid token name 2024-07-27 22:17:48 +07:00
Tal
38c38ec280 Update pr_agent/algo/ai_handlers/litellm_ai_handler.py
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-27 18:03:35 +03:00
Tal
3904eebf85 Update pr_agent/algo/ai_handlers/litellm_ai_handler.py
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-27 18:02:57 +03:00
778d7ce1ed config 2024-07-27 17:54:36 +03:00
3067afbcb3 Update seed handling: log fixed seed usage; adjust default seed and temperature in config 2024-07-27 17:50:59 +03:00
70f7a90226 Add E2E tests workflow for GitHub, GitLab, and Bitbucket; update configuration settings and build-and-test workflow 2024-07-27 17:26:24 +03:00
7eadb45c09 Refactor seed handling logic in litellm_ai_handler to improve readability and error checking 2024-07-27 17:23:42 +03:00
ac247dbc2c Add end-to-end tests for GitHub, GitLab, and Bitbucket apps; update temperature setting usage across tools 2024-07-27 17:19:32 +03:00
Tal
3a77652660 Merge pull request #1061 from MarkRx/feature/bitbucket-server-get-line-link
Implement BitbucketServerProvider.get_line_link
2024-07-27 16:04:09 +03:00
Tal
0bd4c9b78a Merge pull request #1059 from Codium-ai/tr/bitbucket_server
bitbucket server
2024-07-27 16:03:39 +03:00
81d07a55d7 Implement BitbucketServerProvider.get_line_link 2024-07-26 12:34:12 -04:00
652ced5406 bitbucket server 2024-07-26 08:31:21 +03:00
Tal
aaf037570b Update configuration.toml
remove dead codde
2024-07-24 19:56:50 +03:00
Tal
cfa565b5d7 Update index.md 2024-07-24 11:11:39 +03:00
Tal
c8819472cf Merge pull request #1052 from shnskfjwr/fix/remove_bedrock_region
docs: Supports litellm's Amazon Bedrock parameter syntax change
2024-07-23 13:12:29 +03:00
53744af32f docs: add a link to the relevant litellm page 2024-07-23 18:32:30 +09:00
41c6502190 docs: Supports litellm's Amazon Bedrock parameter syntax change 2024-07-23 16:25:26 +09:00
Tal
32604d8103 Merge pull request #1051 from KennyDizi/main
Add support for gpt-4o-mini and gpt-4o-mini-2024-07-18 models
2024-07-23 09:23:56 +03:00
581c95c4ab Add support model gpt-4o-mini-2024-07-18 2024-07-23 07:43:47 +07:00
789c48a216 Add support model gpt-4o-mini 2024-07-23 07:41:04 +07:00
Tal
6b9de6b253 Merge pull request #1048 from woung717/fix/review-task-prompt
fix broken output format(yaml) prompt
2024-07-21 17:51:23 +03:00
003846a90d fixed broken output format(yaml) prompt 2024-07-19 16:02:01 +09:00
d088f9c19a disable 'PUBLISH_OUTPUT_PROGRESS=False' 2024-07-18 11:49:09 +03:00
Tal
a272c761a9 Merge pull request #1046 from Codium-ai/tr/readme_claude
PR-Agent Pro Models
2024-07-18 11:07:27 +03:00
Tal
9449f2aebe Update docs/docs/css/custom.css
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-18 11:05:43 +03:00
Tal
28ea4a685a Update docs/docs/usage-guide/introduction.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-18 11:05:35 +03:00
b798291bc8 Merge remote-tracking branch 'origin/tr/readme_claude' into tr/readme_claude 2024-07-18 10:58:51 +03:00
62df50cf86 docs 2024-07-18 10:58:39 +03:00
917e1607de docs 2024-07-18 10:42:59 +03:00
8f11a19c32 docs 2024-07-18 09:55:57 +03:00
Tal
0f5cccd18f Update docs/docs/usage-guide/PR_agent_pro_models.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-17 22:36:08 +03:00
2be459e576 PR-Agent Pro Models 2024-07-17 22:33:43 +03:00
Tal
cbdb451c95 Merge pull request #1043 from KennyDizi/main
Refactor markdown generation logic to eliminate redundancy
2024-07-17 10:04:24 +03:00
Tal
6871193381 Merge pull request #1042 from 5000164/feature/update-document-about-github-actions
Update GitHub Action documentation for Docker image stability and security
2024-07-17 07:25:32 +03:00
8a7f3501ea Fix duplication code 2024-07-16 18:27:58 +07:00
80bbe23ad5 docs: Add note to pin Docker image by its digest for enhanced security 2024-07-16 18:17:01 +09:00
05f3fa5ebc docs: Update GitHub Action documentation to specify Docker image on Docker Hub 2024-07-16 18:15:56 +09:00
Tal
1b2a2075ae Merge pull request #1041 from KennyDizi/main
Fix data type for `pr_files` field in PR description prompts
2024-07-16 08:09:23 +03:00
3d3b49e3ee remove github.com 2024-07-15 20:18:21 +03:00
174b4b76eb Fix pr_files data type 2024-07-15 12:28:53 +07:00
Tal
2b28153749 Merge pull request #1040 from Codium-ai/tr/review_links
review with links
2024-07-14 09:04:34 +03:00
6151bfac25 key.lower 2024-07-14 09:00:10 +03:00
5d6e1de157 review with links 2024-07-14 08:53:53 +03:00
Tal
ce35d2c313 Merge pull request #1039 from KennyDizi/main
Enhance and clarify descriptions for relevant file and language fields across prompts
2024-07-14 08:10:47 +03:00
b51abe9af7 Use full file path instead of complete file path 2024-07-14 12:01:33 +07:00
20206af1bf Optimize relevant file and language description for pr code suggestion prompt 2024-07-14 10:03:44 +07:00
34ae1f1ab6 Optimize relevant file and language description for pr description prompt 2024-07-14 10:03:31 +07:00
887283632b Optimize relevant file and language description for review prompt 2024-07-14 10:03:19 +07:00
7f84b5738e Optimize relevant file description for add docs prompt 2024-07-14 10:02:00 +07:00
Tal
dc917587ef Merge pull request #1038 from Codium-ai/tr/readme
example best practice
2024-07-12 17:16:37 +03:00
b2710ec029 example best practice 2024-07-12 17:15:00 +03:00
41c48ca5b5 example best practice 2024-07-12 17:07:23 +03:00
Tal
e0012702c6 Merge pull request #1037 from Codium-ai/tr/readme
example best practice
2024-07-12 17:01:24 +03:00
Tal
dfb339ab44 Update docs/docs/usage-guide/EXAMPLE_BEST_PRACTICE.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-12 17:00:43 +03:00
54947573bf example best practice 2024-07-12 16:58:34 +03:00
Tal
228ceff3a6 Merge pull request #1035 from Codium-ai/tr/providers
Tr/providers
2024-07-11 18:47:42 +03:00
Tal
8766140554 Merge pull request #1031 from KennyDizi/main
Fix typo in `pr_files` field type definition within `PRDescription` class
2024-07-11 18:38:59 +03:00
034ec8f53a provider 2024-07-11 18:37:37 +03:00
eccd00b86f github 2024-07-11 18:30:16 +03:00
4b351cfe38 AzureDevopsProvider 2024-07-11 18:24:40 +03:00
734a027702 azure fix 2024-07-11 18:21:21 +03:00
d0948329d3 Fix typo in PRDescription class 2024-07-10 14:37:52 +07:00
6135bf1f53 for example 2024-07-10 07:47:58 +03:00
ea9deccb91 Merge remote-tracking branch 'origin/main' 2024-07-10 07:40:43 +03:00
daa68f3b2f globacl configuration docs 2024-07-10 07:40:29 +03:00
Tal
e82430891c Merge pull request #1030 from R-Mathis/main
add additional google models support
2024-07-10 07:32:08 +03:00
19ca7f887a add additional google models support 2024-07-09 14:29:50 +02:00
Tal
888306c160 Merge pull request #1029 from Codium-ai/tr/best_practice
Tr/best practice
2024-07-09 14:22:59 +03:00
3ef4daafd5 best_practices 2024-07-09 13:59:30 +03:00
f76f750757 best_practices 2024-07-09 07:49:30 +03:00
Tal
055bc4ceec Update improve.md 2024-07-08 17:29:18 +03:00
487efa4bf4 fix link 2024-07-08 09:22:46 +03:00
Tal
050ffcdd06 Merge pull request #1028 from Codium-ai/tr/enterprise
GitHub Enterprise Server
2024-07-08 09:19:13 +03:00
Tal
8f9879cf01 Update docs/docs/overview/pr_agent_pro.md
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-08 09:19:05 +03:00
c3fac86067 GitHub Enterprise Server 2024-07-08 09:07:37 +03:00
9a57d00951 GitHub Enterprise Server 2024-07-08 09:01:56 +03:00
745d0c537c hotfix 2024-07-07 15:07:09 +03:00
5b594dadee pr_types rstrip 2024-07-07 08:08:53 +03:00
Tal
4246792261 Update README.md 2024-07-06 21:18:48 +03:00
Tal
0bf8c1e647 Merge pull request #1025 from Codium-ai/tr/refresh
refresh message
2024-07-05 17:12:55 +03:00
be18152446 refresh 2024-07-05 17:11:13 +03:00
7fc41409d4 refresh 2024-07-05 16:51:35 +03:00
Tal
78bcb72205 Update configuration_options.md 2024-07-04 18:42:23 +03:00
Tal
e35f83bdb6 Merge pull request #1022 from Codium-ai/tr/claude
sonnet-3.5
2024-07-04 12:27:34 +03:00
Tal
20d9d8ad07 Update pr_agent/algo/ai_handlers/litellm_ai_handler.py
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-07-04 12:26:23 +03:00
f3c80891f8 sonnet-3.5 2024-07-04 12:23:36 +03:00
Tal
12973c2c99 Merge pull request #1021 from Codium-ai/tr/margins
increase margins
2024-07-04 12:13:22 +03:00
Tal
1f5c3a4c0f Merge pull request #1020 from Codium-ai/tr/fallbacks
Tr/fallbacks
2024-07-04 12:12:39 +03:00
422b4082b5 No key issues to review 2024-07-03 20:58:25 +03:00
2235a19345 increase margins 2024-07-03 20:53:15 +03:00
e30c70d2ca keys fallback 2024-07-03 20:29:17 +03:00
f7a6e93b6c Merge pull request #1019 from Codium-ai/hl/persistent_suggestions_comment
persistent suggestions
2024-07-03 17:10:21 +03:00
23e6abcdce new line 2024-07-03 17:09:50 +03:00
0bac03496a keys fallback 2024-07-03 17:06:27 +03:00
a228ea8109 fix capitalize 2024-07-03 17:04:01 +03:00
0c3940b6a7 persistent release notes 2024-07-03 16:38:13 +03:00
Tal
b05e15e9ec Merge pull request #1016 from Codium-ai/tr/review_redesign
tests
2024-07-03 08:52:16 +03:00
bea68084b3 ValueError 2024-07-03 08:51:08 +03:00
57abf4ac62 tests 2024-07-03 08:47:59 +03:00
Tal
f0efe4a707 Merge pull request #1011 from Codium-ai/tr/review_redesign
Tr/review redesign
2024-07-03 08:38:27 +03:00
040503039e prompt 2024-07-03 08:37:04 +03:00
3e265682a7 extend additional files 2024-07-03 08:32:37 +03:00
d7c0f87ea5 table 2024-07-03 08:19:58 +03:00
92d040c80f Merge remote-tracking branch 'origin/main' into tr/review_redesign 2024-07-03 07:54:26 +03:00
Tal
96ededd12a Merge pull request #1014 from Codium-ai/tr/description
Tr/description
2024-06-30 20:32:28 +03:00
8d87b41cf2 extend additional files 2024-06-30 20:28:32 +03:00
f058c09a68 extend additional files 2024-06-30 20:20:50 +03:00
f2cb70ea67 extend additional files 2024-06-30 18:38:06 +03:00
3e6263e1cc async calls 2024-06-30 17:33:48 +03:00
3373fb404a review_v2 2024-06-29 21:57:20 +03:00
df02cc1437 Merge remote-tracking branch 'origin/main' into tr/review_redesign
# Conflicts:
#	pr_agent/tools/pr_reviewer.py
2024-06-29 21:55:49 +03:00
Tal
6a5f43f8ce Merge pull request #1005 from KennyDizi/main
Centralize PR Review Title Definition
2024-06-29 21:53:20 +03:00
ebbf9c25b3 review_v2 2024-06-29 21:42:12 +03:00
0dc7bdabd2 review_v2 2024-06-29 21:22:25 +03:00
defe200817 review_v2 2024-06-29 13:08:34 +03:00
bf5673912d APITimeoutError 2024-06-29 11:30:15 +03:00
Tal
089a76c897 Merge pull request #1009 from Codium-ai/tr/fix
fixes
2024-06-28 08:11:08 +03:00
4c444f5c9a fixes 2024-06-28 08:09:52 +03:00
e5aae0d14f fixes 2024-06-28 08:04:35 +03:00
15f854336a s 2024-06-27 09:07:19 +03:00
Tal
056eb3a954 Merge pull request #1004 from Codium-ai/tr/large_pr
Tr/large pr
2024-06-27 08:37:02 +03:00
11abce3ede s 2024-06-27 08:36:40 +03:00
556dc68add s 2024-06-27 08:32:14 +03:00
Tal
b1f728e6b0 Merge pull request #1006 from Codium-ai/tr/readme_updates
s
2024-06-27 08:30:19 +03:00
ca18f85294 s 2024-06-27 08:27:49 +03:00
382da3a5b6 Use descriptive name for the ReviewHeaderTitle enum to reflect its specific purpose related to PR headers 2024-06-27 07:17:26 +07:00
406dcd7b7b Improve code readability by adding a newline after enum definitions 2024-06-27 07:16:54 +07:00
b20f364b15 Change the data structure for prefixes to a list to preserve order 2024-06-27 07:16:26 +07:00
692904bb71 Use ReviewHeaderTitle in lieu of PrReviewTitle 2024-06-27 07:11:57 +07:00
ba963149ac Fix extract PrReviewTitle member value 2024-06-27 07:10:57 +07:00
7348d4144b Rename PrReviewTitle enum 2024-06-27 07:05:03 +07:00
d0315164be Apply PrReviewTitles enum for test file 2024-06-27 07:04:02 +07:00
41607b10ef Apply PrReviewTitles enum for pr review file 2024-06-27 07:03:43 +07:00
2d21df61c7 Apply PrReviewTitles enum for github provider file 2024-06-27 07:03:25 +07:00
c185b7c610 Apply PrReviewTitles enum for algo utils file 2024-06-27 07:03:08 +07:00
3d60954167 Add PrReviewTitles enum 2024-06-27 06:59:49 +07:00
a57896aa94 s 2024-06-26 21:05:00 +03:00
73f0eebb69 s 2024-06-26 20:44:30 +03:00
b1d07be728 s 2024-06-26 20:33:45 +03:00
0f920bcc5b s 2024-06-26 20:11:20 +03:00
55a82382ef Merge remote-tracking branch 'origin/main' 2024-06-26 16:20:16 +03:00
6c2a14d557 fix: correct indentation in PR description preparation logic 2024-06-26 16:20:05 +03:00
Tal
4ab747dbfd Merge pull request #1000 from yamitzky/main
feat: Support Anthropic Calude 3.5 Sonnet on Vertex AI
2024-06-25 16:38:38 +03:00
b814e4a26d feat: Support Anthropic Calude 3.5 Sonnet on Vertex AI 2024-06-25 17:32:17 +09:00
Tal
609fa266cf Merge pull request #998 from R-Mathis/main
Add gemini support for pr-agent
2024-06-24 20:25:26 +03:00
69f6997739 remove extra space 2024-06-24 14:01:33 +02:00
8cc436cbd6 add gemini support for pr agent 2024-06-24 13:48:56 +02:00
384dfc2292 add text bison support for pr agent 2024-06-24 13:28:37 +02:00
40737c3932 add gemini support for pr agent 2024-06-24 12:08:16 +02:00
c46434ac5e add gemini support for pr agent 2024-06-24 12:03:34 +02:00
255c2d8e94 add gemini support for pr ageny 2024-06-24 11:35:41 +02:00
74bb07e9c4 fix: correct indentation in PR description preparation logic 2024-06-23 21:17:34 +03:00
a4db59fadc Merge remote-tracking branch 'origin/main' 2024-06-23 16:53:53 +03:00
2990aac955 docs: update custom labels configuration and usage instructions in describe tool 2024-06-23 16:53:45 +03:00
Tal
afe037e976 Merge pull request #995 from Codium-ai/tr/prompt_bug_fix
fix: update prompt key from `suggestion_content` to `suggestion_summary` in code suggestions
2024-06-23 11:40:03 +03:00
666fcb6264 fix: update prompt key from suggestion_content to suggestion_summary in code suggestions 2024-06-23 11:33:33 +03:00
3f3e9909fe fix: initialize git_provider as an empty dictionary in GitHub App server 2024-06-23 10:03:52 +03:00
Tal
685c443d87 Merge pull request #990 from s1moe2/claude-sonnet
feat: claude 3.5 sonnet support
2024-06-22 08:16:39 +03:00
Tal
c4361ccb01 Merge pull request #989 from Codium-ai/mrT23-patch-7
Update requirements.txt
2024-06-22 08:16:20 +03:00
a3d4d6d86f feat: claude 3.5 sonnet support 2024-06-21 09:30:52 +01:00
Tal
b12554ee84 Update requirements.txt 2024-06-20 18:11:43 +03:00
Tal
29bc0890ab Update requirements.txt 2024-06-20 18:09:39 +03:00
Tal
5fd7ca7d02 Update requirements.txt 2024-06-20 18:01:26 +03:00
Tal
41ffa8df51 Update requirements.txt 2024-06-20 17:45:47 +03:00
47b12d8bbc Update usage guide to clarify ignore settings and provide example usage 2024-06-19 13:59:04 +03:00
Tal
ded8dc3689 Merge pull request #986 from Codium-ai/tr/is_auto
Add `is_auto_command` config check to conditionally publish output an…
2024-06-19 11:57:19 +03:00
9034e18772 Add is_auto_command config check to conditionally publish output and update GitHub App to set this flag 2024-06-19 11:51:10 +03:00
833bb29808 Add context-aware git provider retrieval and refactor related functions 2024-06-19 11:20:00 +03:00
Tal
bdf1be921d Merge pull request #984 from Codium-ai/tr/cache_git_provider
Tr/cache git provider
2024-06-19 11:02:11 +03:00
0c1331f77e Add context-aware git provider retrieval and refactor related functions 2024-06-19 09:49:47 +03:00
164999d83d Add context-aware git provider retrieval and refactor related functions 2024-06-19 09:46:48 +03:00
a710f3ff43 Add context-aware git provider retrieval and refactor related functions 2024-06-19 09:41:53 +03:00
025a14014a Add context-aware git provider retrieval and refactor related functions 2024-06-19 09:36:37 +03:00
5968db67b9 Merge remote-tracking branch 'origin/main' 2024-06-19 08:30:55 +03:00
3affe011fe Update Bitbucket app commands to include suggestions score threshold in documentation and configuration 2024-06-19 08:30:45 +03:00
Tal
c4a653f70a Merge pull request #983 from Codium-ai/ok/gunicorn
Support running GitHub App using gunicorn, adjust Dockerfile accordingly
2024-06-18 22:42:00 +03:00
663604daa5 Support running GitHub App using gunicorn, adjust Dockerfile accordingly 2024-06-18 20:15:48 +03:00
Tal
deda06866d Merge pull request #971 from brianteeman/typos-1
Fix typos/Spelling
2024-06-18 18:00:42 +03:00
e33f2e4c67 Improve wording and clarity in self-review section of improve.md 2024-06-18 17:48:03 +03:00
00b6a67e1e mkdocs 2024-06-18 10:06:58 +03:00
024ef7eea3 mkdocs 2024-06-18 09:53:49 +03:00
3fee687a34 mkdocs 2024-06-18 09:47:25 +03:00
Tal
b2c0c4d654 Merge pull request #981 from Codium-ai/tr/self_review
mkdocs
2024-06-18 09:42:59 +03:00
6b56ea4289 mkdocs 2024-06-18 09:40:05 +03:00
Tal
2a68a90474 Merge pull request #980 from Codium-ai/tr/self_review
mkdocs
2024-06-18 09:32:44 +03:00
de9b21d7bd mkdocs 2024-06-18 09:29:32 +03:00
Tal
612c6ed135 Merge pull request #978 from Codium-ai/tr/self_review
Add self-review checkbox functionality to improve tool and update doc…
2024-06-17 20:30:28 +03:00
6ed65eb82b Add self-review checkbox functionality to improve tool and update documentation 2024-06-17 20:30:14 +03:00
bc09330a44 Add self-review checkbox functionality to improve tool and update documentation 2024-06-17 20:26:09 +03:00
7bd1e5211c Update pr_agent/settings/pr_reviewer_prompts.toml
Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-06-17 09:22:38 +01:00
Tal
8d44804f84 Merge pull request #975 from brianteeman/identify_image_in_comment
identify_image_in_comment
2024-06-17 08:41:35 +03:00
Tal
a4320b6b0d Merge pull request #976 from brianteeman/get_changlog_file
get_changlog_file
2024-06-17 08:39:59 +03:00
Tal
73ec67b14e Merge pull request #974 from brianteeman/repetition_penalty
repetition_penalty
2024-06-17 08:39:16 +03:00
Tal
790dcc552e Merge pull request #972 from brianteeman/overall_success
Overall_success
2024-06-17 08:34:59 +03:00
8463aaac0a get_changlog_file
Correct the spelling

Fix spelling errors now will prevent issues going forward where people have to misspell something on purpose
2024-06-16 17:35:52 +01:00
195f8a03ab identify_image_in_comment
Correct the spelling

Fix spelling errors now will prevent issues going forward where people have to misspell something on purpose
2024-06-16 17:33:23 +01:00
5268a84bcc repetition_penalty
Correct the spelling of this variable.

Fix spelling errors now will prevent issues going forward where people have to misspell something on purpose
2024-06-16 17:28:30 +01:00
e53badbac4 Overall_success
Correct the spelling of this variable.

Fix spelling errors now will prevent issues going forward where people have to misspell something on purpose
2024-06-16 17:12:11 +01:00
a9a27b5a8f Fix typos/Spelling
This simple PR fixes typos and spelling errors in code comments and documentation. It has no functional changes but does at least make the instruction more readable and match the code.
2024-06-16 17:06:30 +01:00
4db428456d Refactor filter_bad_extensions and is_valid_file functions to improve code readability and reusability 2024-06-15 20:10:46 +03:00
925fab474c key_issues_to_review 2024-06-15 19:58:09 +03:00
Tal
a1fb9aac29 Merge pull request #967 from yamitzky/main
Fix ModuleNotFoundError of Vertex AI
2024-06-15 19:51:23 +03:00
Tal
774bba4ed2 Merge pull request #964 from evalphobia/feature/vertexai-calude3
Support models: Anthropic Claude 3 on Vertex AI
2024-06-15 19:49:49 +03:00
Tal
dd8a7200f7 Merge pull request #966 from Codium-ai/tr/ignore
Tr/ignore
2024-06-15 19:48:42 +03:00
33d8b51abd Fix problem of vertex ai 2024-06-13 22:17:12 +09:00
e083841d96 Add file ignore functionality and update documentation for ignore patterns 2024-06-13 13:18:15 +03:00
1070f9583f Add file ignore functionality and update documentation for ignore patterns 2024-06-13 13:07:24 +03:00
bedcc2433c Add file ignore functionality and update documentation for ignore patterns 2024-06-13 13:00:39 +03:00
8ff85a9daf Fix markdown formatting in utils.py by removing extra newlines 2024-06-13 12:45:57 +03:00
58bc54b193 Add file ignore functionality and update documentation for ignore patterns 2024-06-13 12:27:10 +03:00
aa56c0097d Add file ignore functionality and update documentation for ignore patterns 2024-06-13 12:20:21 +03:00
20f6af803c Add file ignore functionality and update documentation for ignore patterns 2024-06-13 12:09:52 +03:00
2076454798 Add file ignore functionality and update documentation for ignore patterns 2024-06-13 12:01:50 +03:00
e367df352b Add file ignore functionality and update documentation for ignore patterns 2024-06-13 11:41:13 +03:00
Tal
a32a12a851 Merge pull request #965 from Codium-ai/tr/reviewer
Tr/reviewer
2024-06-13 09:34:28 +03:00
3a897935ae Merge remote-tracking branch 'origin/main' into tr/reviewer 2024-06-13 09:30:09 +03:00
55b52ad6b2 Add exception handling to process_can_be_split function and update pr_reviewer_prompts.toml formatting 2024-06-13 09:28:51 +03:00
b0f9b96c75 Support models: Anthropic Calude 3 on Vertex AI 2024-06-13 14:34:14 +09:00
Tal
aac7aeabd1 Update PR review prompts and terminology for clarity and consistency (#954)
* Update PR review prompts and terminology for clarity and consistency
2024-06-10 08:44:11 +03:00
306fd3d064 Update PR review prompts and terminology for clarity and consistency 2024-06-10 08:13:48 +03:00
Tal
f1d5587220 Merge pull request #955 from Codium-ai/ok/github_background
Github: work in background
2024-06-09 20:55:02 +03:00
07f21a5511 Github: work in background 2024-06-09 18:14:42 +03:00
1106dccc4f Update PR review prompts and terminology for clarity and consistency 2024-06-09 14:34:51 +03:00
e5f269040e Update PR review prompts and terminology for clarity and consistency 2024-06-09 14:32:50 +03:00
9c8bc6c86a Update PR review prompts and terminology for clarity and consistency 2024-06-09 14:29:32 +03:00
Tal
f4c9d23084 Merge pull request #950 from ryanzll/main
update LangChainOpenAIHandler for langchain version 0.2 and read openai_api_base
2024-06-09 10:15:53 +03:00
25fdf16894 Update PR help message links to new documentation URLs 2024-06-07 08:12:06 +03:00
Tal
12b0df4608 committable checbox (#951)
* Add "apply suggestions" feature to README, improve.md, and configuration.toml
2024-06-06 18:04:40 +03:00
529346b8e0 Merge branch 'main' of https://github.com/ryanzll/pr-agent 2024-06-06 22:28:11 +08:00
b28f66aaa0 1. update LangChainOpenAIHandler to support langchain version 0.2
2. read openai_api_base from settings for llms that compatible with openai
2024-06-06 22:27:01 +08:00
Tal
2e535e42ee Merge pull request #947 from Codium-ai/tr/gitlab_commits
gitlab push_commands
2024-06-05 20:32:11 +03:00
9c6a363a51 gitlab push_commands 2024-06-05 20:31:35 +03:00
75a27d64b4 gitlab push_commands 2024-06-05 20:26:45 +03:00
Tal
4549cb3948 Merge pull request #946 from Codium-ai/tr/gitlab_commits
gitlab push_commands will work
2024-06-05 11:25:57 +03:00
d046c2a939 gitlab push_commands will work 2024-06-05 11:25:10 +03:00
Tal
aed4ed41cc Merge pull request #944 from Codium-ai/tr/gitlab_commits
handle_push_trigger for gitlab
2024-06-04 20:17:27 +03:00
4d96d11ba5 enterprise 2024-06-04 20:15:22 +03:00
faf4576f03 enterprise 2024-06-04 17:08:24 +03:00
0b7dcf03a5 Improve error handling in get_mr_url_from_commit_sha function in gitlab_webhook.py 2024-06-04 16:47:20 +03:00
8e12787fc8 Clear secret provider configuration in configuration.toml 2024-06-04 16:42:54 +03:00
213ced7e18 Add PR evaluation prompt and link to fine-tuning benchmark documentation 2024-06-04 16:37:35 +03:00
Tal
6d6fb67306 Merge pull request #942 from barnett-yuxiang/main
Update Python code formatting, configuration loading, and local model additions
2024-06-04 15:38:20 +03:00
fac8a80c24 Update introduction.md 2024-06-04 17:15:20 +08:00
c53c6aee7f fix wrong provider name 2024-06-04 15:09:30 +08:00
b980168e75 Update pr_description.py 2024-06-04 11:19:18 +08:00
86d901d5a6 Update __init__.py 2024-06-04 11:17:04 +08:00
b1444eb180 format github_action_runner.py 2024-06-04 11:10:13 +08:00
d3a7041f0d update alog/__init__.py 2024-06-04 00:00:22 +08:00
b4f0ad948f Update Python code formatting, configuration loading, and local model additions
1. Code Formatting:
   - Standardized Python code formatting across multiple files to align with PEP 8 guidelines. This includes adjustments to whitespace, line breaks, and inline comments.

2. Configuration Loader Enhancements:
   - Enhanced the `get_settings` function in `config_loader.py` to provide more robust handling of settings retrieval. Added detailed documentation to improve code maintainability and clarity.

3. Model Addition in __init__.py:
   - Added a new model "ollama/llama3" with a token limit to the MAX_TOKENS dictionary in `__init__.py` to support new AI capabilities and configurations.
2024-06-03 23:58:31 +08:00
ab31d2f1f8 Merge pull request #941 from Codium-ai/ok/gitlab_fix
Ok/gitlab fix
2024-06-03 14:16:38 +03:00
2b0dfc6298 Merge remote-tracking branch 'origin/main' into ok/gitlab_fix 2024-06-03 14:15:46 +03:00
76ff49d446 Refactor GitLab webhook initialization to move app setup outside of start function 2024-06-03 12:38:41 +03:00
413547f404 Refactor GitLab webhook handling for async processing and improved logging 2024-06-03 12:36:24 +03:00
Tal
f8feaa0be7 Merge pull request #940 from Codium-ai/tr/benchmark
Add PR evaluation prompt and link to fine-tuning benchmark documentation
2024-06-03 11:39:08 +03:00
09190efb65 Add PR evaluation prompt and link to fine-tuning benchmark documentation 2024-06-03 11:35:39 +03:00
Tal
2746bd4754 Merge pull request #939 from Codium-ai/tr/benchmark
Disable final update message when auto_describe is enabled in GitHub …
2024-06-03 08:37:37 +03:00
4f13007267 Disable final update message when auto_describe is enabled in GitHub Action Runner 2024-06-03 08:15:36 +03:00
Tal
962bb1c23d Update index.md 2024-06-02 11:35:33 +03:00
Tal
e9804c9f0d Merge pull request #938 from Codium-ai/tr/benchmark
Add documentation for PR-Agent code fine-tuning benchmark and update …
2024-06-02 11:32:16 +03:00
f3aa9c02cc Add documentation for PR-Agent code fine-tuning benchmark and update mkdocs.yml 2024-06-02 11:30:56 +03:00
416b150d66 Add documentation for PR-Agent code fine-tuning benchmark and update mkdocs.yml 2024-06-02 11:28:48 +03:00
Tal
83f3cc5033 Merge pull request #933 from MarkRx/bugfix/server-logging-context
Fix some server implementations not properly logging context
2024-06-02 08:29:43 +03:00
Tal
1e1636911f Merge pull request #937 from KennyDizi/main
Optimize document table format
2024-06-01 10:53:47 +03:00
40658cfb7c Removed duplicaiton line 2024-06-01 08:14:25 +07:00
85f6353d15 Optimize for fine-tuning impact table 2024-06-01 08:11:34 +07:00
b9aeb8e443 Fix all markdownlint violations 2024-06-01 08:09:41 +07:00
ea7a84901d Simplify model performance table 2024-06-01 08:05:37 +07:00
Tal
37f6e18953 Merge pull request #932 from MarkRx/feature/valid-file-filter-log
Bitbucket server filter out globally ignored files before attempting diff
2024-05-31 16:33:01 +03:00
Tal
62c6211998 Merge pull request #936 from Codium-ai/mrT23-patch-5
Update README.md
2024-05-31 16:25:58 +03:00
Tal
dc6ae9fa7e Update README.md 2024-05-31 16:25:00 +03:00
Tal
c6e6cbb50e Merge pull request #935 from Codium-ai/tr/benchmark
Add documentation for PR-Agent code fine-tuning benchmark and update …
2024-05-31 16:12:35 +03:00
731c8de4ea Add documentation for PR-Agent code fine-tuning benchmark and update mkdocs.yml 2024-05-31 16:12:25 +03:00
4971071b1f Merge branch 'main' into feature/valid-file-filter-log 2024-05-31 09:10:50 -04:00
c341446015 Add documentation for PR-Agent code fine-tuning benchmark and update mkdocs.yml 2024-05-31 16:09:34 +03:00
Tal
ea9d410c84 Merge pull request #928 from MarkRx/bugfix/bb-server-fixes
Bitbucket Server Provider Fixes
2024-05-31 11:12:41 +03:00
d9a7dae6c4 Better error handling on backgrounp task thread 2024-05-30 20:22:58 -04:00
c9c14c10b0 Fix some server implementations not properly logging context 2024-05-30 20:16:21 -04:00
bd2f2b3a87 Improve bb server error logging 2024-05-30 17:31:46 -04:00
c11ee8643e Bitbucket server filter out globally ignored files before attempting diff 2024-05-30 11:34:39 -04:00
Tal
04d55a6309 Merge pull request #931 from Codium-ai/tr/logs_filter
Add logging for skipping non-code files in GitHub provider
2024-05-30 17:08:58 +03:00
e6c5236156 Add logging for skipping non-code files in GitHub provider 2024-05-30 17:05:30 +03:00
ee90f38501 BB Server fixes. Fix ID not being retrieved, inline comments, url generation, and pr review not working 2024-05-30 10:05:00 -04:00
6e6f54933e Add large_patch_policy configuration and implement patch clipping logic 2024-05-29 14:00:04 +03:00
911c1268fc Add large_patch_policy configuration and implement patch clipping logic 2024-05-29 13:52:44 +03:00
17f46bb53b Add large_patch_policy configuration and implement patch clipping logic 2024-05-29 13:42:44 +03:00
Tal
806ba3f9d8 Merge pull request #926 from eltociear/patch-2
docs: update additional_configurations.md
2024-05-28 08:50:33 +03:00
2a69116767 docs: update additional_configurations.md
Huggingface -> Hugging Face
2024-05-28 12:52:35 +09:00
Tal
b7225c1d10 Update README.md 2024-05-23 16:53:00 +03:00
Tal
ca5efbc52f Merge pull request #918 from Codium-ai/tr/readme2
protections
2024-05-22 21:51:09 +03:00
da44bd7d5e extended_patch 2024-05-22 21:50:00 +03:00
83ff9a0b9b final_update_message 2024-05-22 21:49:33 +03:00
4cd9626217 grammar 2024-05-22 21:47:49 +03:00
Tal
ca9f96a1e3 Merge pull request #916 from Codium-ai/tr/readme2
Chrome Extension readme
2024-05-22 14:41:47 +03:00
811965d841 grammar 2024-05-22 14:40:31 +03:00
39fe6f69d0 Expand and enhance documentation for PR-Agent Chrome Extension, adding detailed feature descriptions and updated images 2024-05-22 14:37:15 +03:00
Tal
66dc9349bd Update README.md 2024-05-21 11:05:33 +03:00
Tal
63340eb75e Cover-Agent 2024-05-21 11:04:37 +03:00
Tal
fab5b6f871 Merge pull request #913 from Codium-ai/tr/final_update
Update default setting of `final_update_message` to false in describe…
2024-05-20 14:30:52 +03:00
71770f3c04 Update default setting of final_update_message to false in describe.md and configuration.toml 2024-05-20 14:25:51 +03:00
Tal
a13cb14e9f Update pyproject.toml 2024-05-20 09:28:05 +03:00
149 changed files with 9068 additions and 2020 deletions

View File

@ -36,6 +36,6 @@ jobs:
- id: test - id: test
name: Test dev docker name: Test dev docker
run: | run: |
docker run --rm codiumai/pr-agent:test pytest -v docker run --rm codiumai/pr-agent:test pytest -v tests/unittest

54
.github/workflows/code_coverage.yaml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Code-coverage
on:
workflow_dispatch:
# push:
# branches:
# - main
pull_request:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- id: checkout
uses: actions/checkout@v2
- id: dockerx
name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- id: build
name: Build dev docker
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
push: false
load: true
tags: codiumai/pr-agent:test
cache-from: type=gha,scope=dev
cache-to: type=gha,mode=max,scope=dev
target: test
- id: code_cov
name: Test dev docker
run: |
docker run --name test_container codiumai/pr-agent:test pytest tests/unittest --cov=pr_agent --cov-report term --cov-report xml:coverage.xml
docker cp test_container:/app/coverage.xml coverage.xml
docker rm test_container
- name: Validate coverage report
run: |
if [ ! -f coverage.xml ]; then
echo "Coverage report not found"
exit 1
fi
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}

46
.github/workflows/e2e_tests.yaml vendored Normal file
View File

@ -0,0 +1,46 @@
name: PR-Agent E2E tests
on:
workflow_dispatch:
# schedule:
# - cron: '0 0 * * *' # This cron expression runs the workflow every night at midnight UTC
jobs:
pr_agent_job:
runs-on: ubuntu-latest
name: PR-Agent E2E GitHub App Test
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- id: build
name: Build dev docker
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
push: false
load: true
tags: codiumai/pr-agent:test
cache-from: type=gha,scope=dev
cache-to: type=gha,mode=max,scope=dev
target: test
- id: test1
name: E2E test github app
run: |
docker run -e GITHUB.USER_TOKEN=${{ secrets.TOKEN_GITHUB }} --rm codiumai/pr-agent:test pytest -v tests/e2e_tests/test_github_app.py
- id: test2
name: E2E gitlab webhook
run: |
docker run -e gitlab.PERSONAL_ACCESS_TOKEN=${{ secrets.TOKEN_GITLAB }} --rm codiumai/pr-agent:test pytest -v tests/e2e_tests/test_gitlab_webhook.py
- id: test3
name: E2E bitbucket app
run: |
docker run -e BITBUCKET.USERNAME=${{ secrets.BITBUCKET_USERNAME }} -e BITBUCKET.PASSWORD=${{ secrets.BITBUCKET_PASSWORD }} --rm codiumai/pr-agent:test pytest -v tests/e2e_tests/test_bitbucket_app.py

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
.idea/ .idea/
.lsp/
.vscode/
venv/ venv/
pr_agent/settings/.secrets.toml pr_agent/settings/.secrets.toml
__pycache__ __pycache__
@ -6,4 +8,4 @@ dist/
*.egg-info/ *.egg-info/
build/ build/
.DS_Store .DS_Store
docs/.cache/ docs/.cache/

View File

@ -1,3 +1,6 @@
[pr_reviewer] [pr_reviewer]
enable_review_labels_effort = true enable_review_labels_effort = true
enable_auto_approval = true enable_auto_approval = true
[config]
model="claude-3-5-sonnet"

176
README.md
View File

@ -10,24 +10,26 @@
</picture> </picture>
<br/> <br/>
CodiumAI PR-Agent aims to help efficiently review and handle pull requests, by providing AI feedbacks and suggestions CodiumAI PR-Agent aims to help efficiently review and handle pull requests, by providing AI feedback and suggestions
</div> </div>
[![GitHub license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE) [![GitHub license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
[![Static Badge](https://img.shields.io/badge/Chrome-Extension-violet)](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) [![Static Badge](https://img.shields.io/badge/Chrome-Extension-violet)](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl)
[![Static Badge](https://img.shields.io/badge/Code-Benchmark-blue)](https://pr-agent-docs.codium.ai/finetuning_benchmark/)
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.com/channels/1057273017547378788/1126104260430528613) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.com/channels/1057273017547378788/1126104260430528613)
[![Twitter](https://img.shields.io/twitter/follow/codiumai)](https://twitter.com/codiumai) [![Twitter](https://img.shields.io/twitter/follow/codiumai)](https://twitter.com/codiumai)
[![Cheat Sheet](https://img.shields.io/badge/Cheat-Sheet-red)](https://www.codium.ai/images/pr_agent/cheat_sheet.pdf)
<a href="https://github.com/Codium-ai/pr-agent/commits/main"> <a href="https://github.com/Codium-ai/pr-agent/commits/main">
<img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20"> <img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20">
</a> </a>
</div> </div>
### [Documentation](https://pr-agent-docs.codium.ai/) ### [Documentation](https://pr-agent-docs.codium.ai/)
- See the [Installation Guide](https://pr-agent-docs.codium.ai/installation/) for instructions on installing PR-Agent on different platforms. - See the [Installation Guide](https://qodo-merge-docs.qodo.ai/installation/) for instructions on installing PR-Agent on different platforms.
- See the [Usage Guide](https://pr-agent-docs.codium.ai/usage-guide/) for instructions on running PR-Agent tools via different interfaces, such as CLI, PR Comments, or by automatically triggering them when a new PR is opened. - See the [Usage Guide](https://qodo-merge-docs.qodo.ai/usage-guide/) for instructions on running PR-Agent tools via different interfaces, such as CLI, PR Comments, 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, and the available configurations for each tool. - See the [Tools Guide](https://qodo-merge-docs.qodo.ai/tools/) for a detailed description of the different tools, and the available configurations for each tool.
## Table of Contents ## Table of Contents
@ -35,75 +37,92 @@ CodiumAI PR-Agent aims to help efficiently review and handle pull requests, by p
- [Overview](#overview) - [Overview](#overview)
- [Example results](#example-results) - [Example results](#example-results)
- [Try it now](#try-it-now) - [Try it now](#try-it-now)
- [PR-Agent Pro 💎](#pr-agent-pro-) - [PR-Agent Pro 💎](https://pr-agent-docs.codium.ai/overview/pr_agent_pro/)
- [How it works](#how-it-works) - [How it works](#how-it-works)
- [Why use PR-Agent?](#why-use-pr-agent) - [Why use PR-Agent?](#why-use-pr-agent)
## News and Updates ## News and Updates
### May 19, 2024 ### September 21, 2024
GPT-4o is now the default fast model ("Turbo"). This model will be used for all commands except `review` and `improve`, which will still use "GPT-4-2024-04-09", since they are harder and would still benefit from the larger model. Need help with PR-Agent? New feature - simply comment `/help "your question"` in a pull request, and PR-Agent will provide you with the [relevant documentation](https://github.com/Codium-ai/pr-agent/pull/1241#issuecomment-2365259334).
### May 12, 2024 <kbd><img src="https://www.codium.ai/images/pr_agent/pr_help_chat.png" width="768"></kbd>
Inspired by [AlphaCodium](https://github.com/Codium-ai/AlphaCodium) flow engineering scheme, PR-Agent now performs **self-reflection** on the code suggestions it provides,
enabling to remove invalid suggestions, and score the valid ones. The suggestions will be presented sorted by their score, enabling to focus on the most important ones first.
You can also choose to automatically remove suggestions below a certain importance score threshold, by setting the `pr_code_suggestions.suggestions_score_threshold` [configuration](https://pr-agent-docs.codium.ai/tools/improve/#configuration-options).
<kbd><img src="https://codium.ai/images/pr_agent/self_reflection1.png" width="512"></kbd>
<kbd><img src="https://codium.ai/images/pr_agent/self_reflection2.png" width="512"></kbd>
### May 2, 2024 ### September 12, 2024
Check out the new [PR-Agent Chrome Extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) 🚀🚀🚀 [Dynamic context](https://pr-agent-docs.codium.ai/core-abilities/dynamic_context/) is now the default option for context extension.
This feature enables PR-Agent to dynamically adjusting the relevant context for each code hunk, while avoiding overflowing the model with too much information.
This toolbar integrates seamlessly with your GitHub environment, allowing you to access PR-Agent tools [directly from the GitHub interface](https://www.youtube.com/watch?v=gT5tli7X4H4). ### September 3, 2024
You can also easily export your chosen configuration, and use it for the automatic commands.
New version of PR-Agent, v0.24 was released. See the [release notes](https://github.com/Codium-ai/pr-agent/releases/tag/v0.24) for more information.
### August 26, 2024
New version of [PR Agent Chrome Extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) was released, with full support of context-aware **PR Chat**. This novel feature is free to use for any open-source repository. See more details in [here](https://pr-agent-docs.codium.ai/chrome-extension/#pr-chat).
<kbd><img src="https://www.codium.ai/images/pr_agent/pr_chat_1.png" width="768"></kbd>
<kbd><img src="https://www.codium.ai/images/pr_agent/pr_chat_2.png" width="768"></kbd>
### August 11, 2024
Increased PR context size for improved results, and enabled [asymmetric context](https://github.com/Codium-ai/pr-agent/pull/1114/files#diff-9290a3ad9a86690b31f0450b77acd37ef1914b41fabc8a08682d4da433a77f90R69-R70)
### August 10, 2024
Added support for [Azure devops pipeline](https://pr-agent-docs.codium.ai/installation/azure/) - you can now easily run PR-Agent as an Azure devops pipeline, without needing to set up your own server.
### August 5, 2024
Added support for [GitLab pipeline](https://pr-agent-docs.codium.ai/installation/gitlab/#run-as-a-gitlab-pipeline) - you can now run easily PR-Agent as a GitLab pipeline, without needing to set up your own server.
### July 28, 2024
(1) improved support for bitbucket server - [auto commands](https://github.com/Codium-ai/pr-agent/pull/1059) and [direct links](https://github.com/Codium-ai/pr-agent/pull/1061)
(2) custom models are now [supported](https://pr-agent-docs.codium.ai/usage-guide/changing_a_model/#custom-models)
<kbd><img src="https://codium.ai/images/pr_agent/toolbar1.png" width="512"></kbd>
<kbd><img src="https://codium.ai/images/pr_agent/toolbar2.png" width="512"></kbd>
## Overview ## Overview
<div style="text-align:left;"> <div style="text-align:left;">
Supported commands per platform: Supported commands per platform:
| | | GitHub | Gitlab | Bitbucket | Azure DevOps | | | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|-------|---------------------------------------------------------------------------------------------------------|:--------------------:|:--------------------:|:--------------------:|:--------------------:| |-------|---------------------------------------------------------------------------------------------------------|:--------------------:|:--------------------:|:--------------------:|:------------:|
| TOOLS | Review | ✅ | ✅ | ✅ | | | TOOLS | Review | ✅ | ✅ | ✅ | |
| | ⮑ Incremental | ✅ | | | | | | ⮑ Incremental | ✅ | | | |
| | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.ai/tools/review/#soc2-ticket-compliance) 💎 | ✅ | ✅ | ✅ | | | | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.ai/tools/review/#soc2-ticket-compliance) 💎 | ✅ | ✅ | ✅ | |
| | Describe | ✅ | ✅ | ✅ | | | | Describe | ✅ | ✅ | ✅ | |
| | ⮑ [Inline File Summary](https://pr-agent-docs.codium.ai/tools/describe#inline-file-summary) 💎 | ✅ | | | | | | ⮑ [Inline File Summary](https://pr-agent-docs.codium.ai/tools/describe#inline-file-summary) 💎 | ✅ | | | |
| | Improve | ✅ | ✅ | ✅ | | | | Improve | ✅ | ✅ | ✅ | |
| | ⮑ Extended | ✅ | ✅ | ✅ | | | | ⮑ Extended | ✅ | ✅ | ✅ | |
| | Ask | ✅ | ✅ | ✅ | | | | Ask | ✅ | ✅ | ✅ | |
| | ⮑ [Ask on code lines](https://pr-agent-docs.codium.ai/tools/ask#ask-lines) | ✅ | ✅ | | | | | ⮑ [Ask on code lines](https://pr-agent-docs.codium.ai/tools/ask#ask-lines) | ✅ | ✅ | | |
| | [Custom Prompt](https://pr-agent-docs.codium.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | | | | [Custom Prompt](https://pr-agent-docs.codium.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | |
| | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | ✅ | ✅ | | | | | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | ✅ | ✅ | | |
| | Reflect and Review | ✅ | ✅ | ✅ | | | | Reflect and Review | ✅ | ✅ | ✅ | |
| | Update CHANGELOG.md | ✅ | ✅ | ✅ | | | | Update CHANGELOG.md | ✅ | ✅ | ✅ | |
| | Find Similar Issue | ✅ | | | | | | Find Similar Issue | ✅ | | | |
| | [Add PR Documentation](https://pr-agent-docs.codium.ai/tools/documentation/) 💎 | ✅ | ✅ | | | | | [Add PR Documentation](https://pr-agent-docs.codium.ai/tools/documentation/) 💎 | ✅ | ✅ | | |
| | [Custom Labels](https://pr-agent-docs.codium.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | | | | [Custom Labels](https://pr-agent-docs.codium.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | |
| | [Analyze](https://pr-agent-docs.codium.ai/tools/analyze/) 💎 | ✅ | ✅ | | | | | [Analyze](https://pr-agent-docs.codium.ai/tools/analyze/) 💎 | ✅ | ✅ | | |
| | [CI Feedback](https://pr-agent-docs.codium.ai/tools/ci_feedback/) 💎 | ✅ | | | | | | [CI Feedback](https://pr-agent-docs.codium.ai/tools/ci_feedback/) 💎 | ✅ | | | |
| | [Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 | ✅ | | | | | | [Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 | ✅ | | | |
| | | | | | | | | | | | | |
| USAGE | CLI | ✅ | ✅ | ✅ | | | USAGE | CLI | ✅ | ✅ | ✅ | |
| | App / webhook | ✅ | ✅ | ✅ | | | | App / webhook | ✅ | ✅ | ✅ | |
| | Tagging bot | ✅ | | | | | | Tagging bot | ✅ | | | |
| | Actions | ✅ | | ✅ | | | | Actions | ✅ |✅| ✅ |✅|
| | | | | | | | | | | | | |
| CORE | PR compression | ✅ | ✅ | ✅ | | | CORE | PR compression | ✅ | ✅ | ✅ | |
| | Repo language prioritization | ✅ | ✅ | ✅ | | | | Repo language prioritization | ✅ | ✅ | ✅ | |
| | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | | | | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | |
| | Multiple models support | ✅ | ✅ | ✅ | | | | Multiple models support | ✅ | ✅ | ✅ | |
| | [Static code analysis](https://pr-agent-docs.codium.ai/core-abilities/#static-code-analysis) 💎 | ✅ | ✅ | ✅ | | | | [Static code analysis](https://pr-agent-docs.codium.ai/core-abilities/#static-code-analysis) 💎 | ✅ | ✅ | ✅ | |
| | [Global and wiki configurations](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) 💎 | ✅ | ✅ | ✅ | | | | [Global and wiki configurations](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) 💎 | ✅ | ✅ | ✅ | |
| | [PR interactive actions](https://www.codium.ai/images/pr_agent/pr-actions.mp4) 💎 | ✅ | | | | | | [PR interactive actions](https://www.codium.ai/images/pr_agent/pr-actions.mp4) 💎 | ✅ | | | |
- 💎 means this feature is available only in [PR-Agent Pro](https://www.codium.ai/pricing/) - 💎 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;) [//]: # (- Support for additional git providers is described in [here]&#40;./docs/Full_environments.md&#41;)
@ -222,7 +241,11 @@ For example, add a comment to any pull request with the following text:
``` ```
@CodiumAI-Agent /review @CodiumAI-Agent /review
``` ```
and the agent will respond with a review of your PR and the agent will respond with a review of your PR.
Note that this is a promotional bot, suitable only for initial experimentation.
It does not have 'edit' access to your repo, for example, so it cannot update the PR description or add labels (`@CodiumAI-Agent /describe` will publish PR description as a comment). In addition, the bot cannot be used on private repositories, as it does not have access to the files there.
![Review generation process](https://www.codium.ai/images/demo-2.gif) ![Review generation process](https://www.codium.ai/images/demo-2.gif)
@ -232,43 +255,6 @@ Note that when you set your own PR-Agent or use CodiumAI hosted PR-Agent, there
--- ---
[//]: # (## Installation)
[//]: # (To use your own version of PR-Agent, you first need to acquire two tokens:)
[//]: # ()
[//]: # (1. An OpenAI key from [here]&#40;https://platform.openai.com/&#41;, with access to GPT-4.)
[//]: # (2. A GitHub personal access token &#40;classic&#41; with the repo scope.)
[//]: # ()
[//]: # (There are several ways to use PR-Agent:)
[//]: # ()
[//]: # (**Locally**)
[//]: # (- [Using pip package]&#40;https://pr-agent-docs.codium.ai/installation/locally/#using-pip-package&#41;)
[//]: # (- [Using Docker image]&#40;https://pr-agent-docs.codium.ai/installation/locally/#using-docker-image&#41;)
[//]: # (- [Run from source]&#40;https://pr-agent-docs.codium.ai/installation/locally/#run-from-source&#41;)
[//]: # ()
[//]: # (**GitHub specific methods**)
[//]: # (- [Run as a GitHub Action]&#40;https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-action&#41;)
[//]: # (- [Run as a GitHub App]&#40;https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-app&#41;)
[//]: # ()
[//]: # (**GitLab specific methods**)
[//]: # (- [Run a GitLab webhook server]&#40;https://pr-agent-docs.codium.ai/installation/gitlab/&#41;)
[//]: # ()
[//]: # (**BitBucket specific methods**)
[//]: # (- [Run as a Bitbucket Pipeline]&#40;https://pr-agent-docs.codium.ai/installation/bitbucket/&#41;)
## PR-Agent Pro 💎 ## 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: [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:
@ -276,7 +262,7 @@ Note that when you set your own PR-Agent or use CodiumAI hosted PR-Agent, there
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. 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. 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. 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. See [here](https://qodo-merge-docs.qodo.ai/overview/pr_agent_pro/) for a list of features available in PR-Agent Pro.

5
codecov.yml Normal file
View File

@ -0,0 +1,5 @@
comment: false
coverage:
status:
patch: false
project: false

View File

@ -1,41 +1,42 @@
FROM python:3.10 as base FROM python:3.12.3 AS base
WORKDIR /app WORKDIR /app
ADD docs/chroma_db.zip /app/docs/chroma_db.zip
ADD pyproject.toml . ADD pyproject.toml .
ADD requirements.txt . ADD requirements.txt .
RUN pip install . && rm pyproject.toml requirements.txt RUN pip install . && rm pyproject.toml requirements.txt
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
FROM base as github_app FROM base AS github_app
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_app.py"] CMD ["python", "-m", "gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "pr_agent/servers/gunicorn_config.py", "--forwarded-allow-ips", "*", "pr_agent.servers.github_app:app"]
FROM base as bitbucket_app FROM base AS bitbucket_app
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_app.py"] CMD ["python", "pr_agent/servers/bitbucket_app.py"]
FROM base as bitbucket_server_webhook FROM base AS bitbucket_server_webhook
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_server_webhook.py"] CMD ["python", "pr_agent/servers/bitbucket_server_webhook.py"]
FROM base as github_polling FROM base AS github_polling
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_polling.py"] CMD ["python", "pr_agent/servers/github_polling.py"]
FROM base as gitlab_webhook 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 FROM base AS azure_devops_webhook
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/azuredevops_server_webhook.py"] 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
ADD pr_agent pr_agent ADD pr_agent pr_agent
ADD tests tests ADD tests tests
FROM base as cli FROM base AS cli
ADD pr_agent pr_agent ADD pr_agent pr_agent
ENTRYPOINT ["python", "pr_agent/cli.py"] ENTRYPOINT ["python", "pr_agent/cli.py"]

View File

@ -1 +1 @@
# [Visit Our Docs Portal](https://pr-agent-docs.codium.ai/) # [Visit Our Docs Portal](https://qodo-merge-docs.qodo.ai/)

BIN
docs/chroma_db.zip Normal file

Binary file not shown.

View File

@ -1 +1 @@
pr-agent-docs.codium.ai qodo-merge-docs.qodo.ai

View File

@ -0,0 +1,5 @@
We take your code's security and privacy seriously:
- The Chrome extension will not send your code to any external servers.
- For private repositories, we will first validate the user's identity and permissions. After authentication, we generate responses using the existing Qodo Merge Pro integration.

View File

@ -0,0 +1,51 @@
### PR chat
The PR-Chat feature allows to freely chat with your PR code, within your GitHub environment.
It will seamlessly use the PR as context to your chat session, and provide AI-powered feedback.
To enable private chat, simply install the Qodo Merge Chrome extension. After installation, each PR's file-changed tab will include a chat box, where you may ask questions about your code.
This chat session is **private**, and won't be visible to other users.
All open-source repositories are supported.
For private repositories, you will also need to install Qodo Merge Pro, After installation, make sure to open at least one new PR to fully register your organization. Once done, you can chat with both new and existing PRs across all installed repositories.
#### Context-aware PR chat
Qodo Merge constructs a comprehensive context for each pull request, incorporating the PR description, commit messages, and code changes with extended dynamic context. This contextual information, along with additional PR-related data, forms the foundation for an AI-powered chat session. The agent then leverages this rich context to provide intelligent, tailored responses to user inquiries about the pull request.
<img src="https://codium.ai/images/pr_agent/pr_chat_1.png" width="768">
<img src="https://codium.ai/images/pr_agent/pr_chat_2.png" width="768">
### Toolbar extension
With Qodo Merge Chrome extension, it's [easier than ever](https://www.youtube.com/watch?v=gT5tli7X4H4) to interactively configure and experiment with the different tools and configuration options.
For private repositories, after you found the setup that works for you, you can also easily export it as a persistent configuration file, and use it for automatic commands.
<img src="https://codium.ai/images/pr_agent/toolbar1.png" width="512">
<img src="https://codium.ai/images/pr_agent/toolbar2.png" width="512">
### Qodo Merge filters
Qodo Merge filters is a sidepanel option. that allows you to filter different message in the conversation tab.
For example, you can choose to present only message from Qodo Merge, or filter those messages, focusing only on user's comments.
<img src="https://codium.ai/images/pr_agent/pr_agent_filters1.png" width="256">
<img src="https://codium.ai/images/pr_agent/pr_agent_filters2.png" width="256">
### Enhanced code suggestions
Qodo Merge Chrome extension adds the following capabilities to code suggestions tool's comments:
- Auto-expand the table when you are viewing a code block, to avoid clipping.
- Adding a "quote-and-reply" button, that enables to address and comment on a specific suggestion (for example, asking the author to fix the issue)
<img src="https://codium.ai/images/pr_agent/chrome_extension_code_suggestion1.png" width="512">
<img src="https://codium.ai/images/pr_agent/chrome_extension_code_suggestion2.png" width="512">

View File

@ -1,28 +1,14 @@
## PR-Agent chrome extension [Qodo Merge Chrome extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) is a collection of tools that integrates seamlessly with your GitHub environment, aiming to enhance your Git usage experience, and providing AI-powered capabilities to your PRs.
- PR-Agent Chrome extension is a toolbar that integrates seamlessly with your GitHub environment, allowing you to access PR-Agent tools directly from the GitHub interface. With a single-click installation you will gain access to a context-aware chat on your pull requests code, a toolbar extension with multiple AI feedbacks, Qodo Merge filters, and additional abilities.
- With PR-Agent Chrome extension, it's [easier than ever](https://www.youtube.com/watch?v=gT5tli7X4H4) to interactively experiment with the different tools and configuration options.
- After you found the setup that works for you, you can also easily export it as a persistent configuration file, and use it for the automatic commands.
<kbd><img src="https://codium.ai/images/pr_agent/toolbar1.png" width="512"></kbd> The extension is powered by top code models like Claude 3.5 Sonnet and GPT4. All the extension's features are free to use on public repositories.
<kbd><img src="https://codium.ai/images/pr_agent/toolbar2.png" width="512"></kbd> For private repositories, you will need to install [Qodo Merge Pro](https://github.com/apps/codiumai-pr-agent-pro) in addition to the extension (Quick GitHub app setup with a 14-day free trial. No credit card needed).
For a demonstration of how to install Qodo Merge Pro and use it with the Chrome extension, please refer to the tutorial video at the provided [link](https://codium.ai/images/pr_agent/private_repos.mp4).
## Installation <img src="https://codium.ai/images/pr_agent/PR-AgentChat.gif" width="768">
Go to the marketplace and install the extension: ### Supported browsers
[PR-Agent Chrome Extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl)
## Pre-requisites The extension is supported on all Chromium-based browsers, including Google Chrome, Arc, Opera, Brave, and Microsoft Edge.
The PR-Agent Chrome extension will work on any repo where you have previously [installed PR-Agent](https://pr-agent-docs.codium.ai/installation/) (both for free and pro users).
## Data privacy and security
The PR-Agent Chrome extension only modifies the visual appearance of a GitHub PR screen. It does not transmit any user's repo or pull request code. Code is only sent for processing when a user submits a GitHub comment that activates a PR-Agent tool, in accordance with the standard [privacy policy of PR-Agent](https://pr-agent-docs.codium.ai/#pr-agent-chrome-extension).
## Roadmap
Stay tuned ...
<kbd><img src="https://codium.ai/images/pr_agent/chrome_extension_roadmap.png" width="512"></kbd>

View File

@ -0,0 +1,2 @@
## Overview
TBD

View File

@ -0,0 +1,47 @@
## Overview - PR Compression Strategy
There are two scenarios:
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)
For both scenarios, we first use the following strategy
#### Repo language prioritization strategy
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)
2. Given the main languages used in the repo
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]]```
### Small PR
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)
2. We Expand the surrounding context of each patch to 3 lines above and below the patch
### Large PR
#### Motivation
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.
#### Compression strategy
We prioritize additions over deletions:
- 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
#### 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:
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
![Core Abilities](https://codium.ai/images/git_patch_logic.png){width=768}

View File

@ -0,0 +1,72 @@
## TL;DR
Qodo Merge uses an **asymmetric and dynamic context strategy** to improve AI analysis of code changes in pull requests.
It provides more context before changes than after, and dynamically adjusts the context based on code structure (e.g., enclosing functions or classes).
This approach balances providing sufficient context for accurate analysis, while avoiding needle-in-the-haystack information overload that could degrade AI performance or exceed token limits.
## Introduction
Pull request code changes are retrieved in a unified diff format, showing three lines of context before and after each modified section, with additions marked by '+' and deletions by '-'.
```
@@ -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...
@@ -26,2 +26,4 @@ def func2():
...
```
This unified diff format can be challenging for AI models to interpret accurately, as it provides limited context for understanding the full scope of code changes.
The presentation of code using '+', '-', and ' ' symbols to indicate additions, deletions, and unchanged lines respectively also differs from the standard code formatting typically used to train AI models.
## Challenges of expanding the context window
While expanding the context window is technically feasible, it presents a more fundamental trade-off:
Pros:
- Enhanced context allows the model to better comprehend and localize the code changes, results (potentially) in more precise analysis and suggestions. Without enough context, the model may struggle to understand the code changes and provide relevant feedback.
Cons:
- Excessive context may overwhelm the model with extraneous information, creating a "needle in a haystack" scenario where focusing on the relevant details (the code that actually changed) becomes challenging.
LLM quality is known to degrade when the context gets larger.
Pull requests often encompass multiple changes across many files, potentially spanning hundreds of lines of modified code. This complexity presents a genuine risk of overwhelming the model with excessive context.
- Increased context expands the token count, increasing processing time and cost, and may prevent the model from processing the entire pull request in a single pass.
## Asymmetric and dynamic context
To address these challenges, Qodo Merge employs an **asymmetric** and **dynamic** context strategy, providing the model with more focused and relevant context information for each code change.
**Asymmetric:**
We start by recognizing that the context preceding a code change is typically more crucial for understanding the modification than the context following it.
Consequently, Qodo Merge implements an asymmetric context policy, decoupling the context window into two distinct segments: one for the code before the change and another for the code after.
By independently adjusting each context window, Qodo Merge can supply the model with a more tailored and pertinent context for individual code changes.
**Dynamic:**
We also employ a "dynamic" context strategy.
We start by recognizing that the optimal context for a code change often corresponds to its enclosing code component (e.g., function, class), rather than a fixed number of lines.
Consequently, we dynamically adjust the context window based on the code's structure, ensuring the model receives the most pertinent information for each modification.
To prevent overwhelming the model with excessive context, we impose a limit on the number of lines searched when identifying the enclosing component.
This balance allows for comprehensive understanding while maintaining efficiency and limiting context token usage.
## Appendix - relevant configuration options
```
[config]
patch_extension_skip_types =[".md",".txt"] # Skip files with these extensions when trying to extend the context
allow_dynamic_context=true # Allow dynamic context extension
max_extra_lines_before_dynamic_context = 8 # will try to include up to X extra lines before the hunk in the patch, until we reach an enclosing function or class
patch_extra_lines_before = 3 # Number of extra lines (+3 default ones) to include before each hunk in the patch
patch_extra_lines_after = 1 # Number of extra lines (+3 default ones) to include after each hunk in the patch
```

View File

@ -0,0 +1,44 @@
# Overview - Impact Evaluation 💎
Demonstrating the return on investment (ROI) of AI-powered initiatives is crucial for modern organizations.
To address this need, Qodo Merge has developed an AI impact measurement tools and metrics, providing advanced analytics to help businesses quantify the tangible benefits of AI adoption in their PR review process.
## Auto Impact Validator - Real-Time Tracking of Implemented Qodo Merge Suggestions
### How It Works
When a user pushes a new commit to the pull request, Qodo Merge automatically compares the updated code against the previous suggestions, marking them as implemented if the changes address these recommendations, whether directly or indirectly:
1. **Direct Implementation:** The user directly addresses the suggestion as-is in the PR, either by clicking on the "apply code suggestion" checkbox or by making the changes manually.
2. **Indirect Implementation:** Qodo Merge recognizes when a suggestion's intent is fulfilled, even if the exact code changes differ from the original recommendation. It marks these suggestions as implemented, acknowledging that users may achieve the same goal through alternative solutions.
### Real-Time Visual Feedback
Upon confirming that a suggestion was implemented, Qodo Merge automatically adds a ✅ (check mark) to the relevant suggestion, enabling transparent tracking of Qodo Merge's impact analysis.
Qodo Merge will also add, inside the relevant suggestions, an explanation of how the new code was impacted by each suggestion.
![Suggestion_checkmark](https://codium.ai/images/pr_agent/auto_suggestion_checkmark.png){width=512}
### Dashboard Metrics
The dashboard provides macro-level insights into the overall impact of Qodo Merge on the pull-request process with key productivity metrics.
By offering clear, data-driven evidence of Qodo Merge's impact, it empowers leadership teams to make informed decisions about the tool's effectiveness and ROI.
Here are key metrics that the dashboard tracks:
#### Qodo Merge Impacts per 1K Lines
![Dashboard](https://codium.ai/images/pr_agent/impacts_per_1k_llines.png){width=512}
> Explanation: for every 1K lines of code (additions/edits), Qodo Merge had on average ~X suggestions implemented.
**Why This Metric Matters:**
1. **Standardized and Comparable Measurement:** By measuring impacts per 1K lines of code additions, you create a standardized metric that can be compared across different projects, teams, customers, and time periods. This standardization is crucial for meaningful analysis, benchmarking, and identifying where Qodo Merge is most effective.
2. **Accounts for PR Variability and Incentivizes Quality:** This metric addresses the fact that "Not all PRs are created equal." By normalizing against lines of code rather than PR count, you account for the variability in PR sizes and focus on the quality and impact of suggestions rather than just the number of PRs affected.
3. **Quantifies Value and ROI:** The metric directly correlates with the value Qodo Merge is providing, showing how frequently it offers improvements relative to the amount of new code being written. This provides a clear, quantifiable way to demonstrate Qodo Merge's return on investment to stakeholders.
#### Suggestion Effectiveness Across Categories
![Impacted_Suggestion_Score](https://codium.ai/images/pr_agent/impact_by_category.png){width=512}
> Explanation: This chart illustrates the distribution of implemented suggestions across different categories, enabling teams to better understand Qodo Merge's impact on various aspects of code quality and development practices.
#### Suggestion Score Distribution
![Impacted_Suggestion_Score](https://codium.ai/images/pr_agent/impacted_score_dist.png){width=512}
> Explanation: The distribution of the suggestion score for the implemented suggestions, ensuring that higher-scored suggestions truly represent more significant improvements.

View File

@ -1,52 +1,12 @@
## PR Compression Strategy # Core Abilities
There are two scenarios: Qodo Merge utilizes a variety of core abilities to provide a comprehensive and efficient code review experience. These abilities include:
1. The PR is small enough to fit in a single prompt (including system and user prompt) - [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/)
2. The PR is too large to fit in a single prompt (including system and user prompt) - [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/)
- [Self-reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/)
For both scenarios, we first use the following strategy - [Impact evaluation](https://qodo-merge-docs.qodo.ai/core-abilities/impact_evaluation/)
- [Interactivity](https://qodo-merge-docs.qodo.ai/core-abilities/interactivity/)
#### Repo language prioritization strategy - [Compression strategy](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/)
We prioritize the languages of the repo based on the following criteria: - [Code-oriented YAML](https://qodo-merge-docs.qodo.ai/core-abilities/code_oriented_yaml/)
- [Static code analysis](https://qodo-merge-docs.qodo.ai/core-abilities/static_code_analysis/)
1. Exclude binary files and non code files (e.g. images, pdfs, etc) - [Code fine-tuning benchmark](https://qodo-merge-docs.qodo.ai/finetuning_benchmark/)
2. Given the main languages used in the repo
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]]```
### Small PR
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)
2. We Expand the surrounding context of each patch to 3 lines above and below the patch
### Large PR
#### Motivation
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.
#### Compression strategy
We prioritize additions over deletions:
- 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
#### 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:
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
![Core Abilities](https://codium.ai/images/git_patch_logic.png){width=768}
## YAML Prompting
TBD
## Static Code Analysis 💎
TBD

View File

@ -0,0 +1,2 @@
## Interactive invocation 💎
TBD

View File

@ -0,0 +1,56 @@
## Local and global metadata injection with multi-stage analysis
(1)
Qodo Merge initially retrieves for each PR the following data:
- PR title and branch name
- PR original description
- Commit messages history
- PR diff patches, in [hunk diff](https://loicpefferkorn.net/2014/02/diff-files-what-are-hunks-and-how-to-extract-them/) format
- The entire content of the files that were modified in the PR
!!! tip "Tip: Organization-level metadata"
In addition to the inputs above, Qodo Merge can incorporate supplementary preferences provided by the user, like [`extra_instructions` and `organization best practices`](https://qodo-merge-docs.qodo.ai/tools/improve/#extra-instructions-and-best-practices). This information can be used to enhance the PR analysis.
(2)
By default, the first command that Qodo Merge executes is [`describe`](https://qodo-merge-docs.qodo.ai/tools/describe/), which generates three types of outputs:
- PR Type (e.g. bug fix, feature, refactor, etc)
- PR Description - a bullet point summary of the PR
- Changes walkthrough - for each modified file, provide a one-line summary followed by a detailed bullet point list of the changes.
These AI-generated outputs are now considered as part of the PR metadata, and can be used in subsequent commands like `review` and `improve`.
This effectively enables multi-stage chain-of-thought analysis, without doing any additional API calls which will cost time and money.
For example, when generating code suggestions for different files, Qodo Merge can inject the AI-generated ["Changes walkthrough"](https://github.com/Codium-ai/pr-agent/pull/1202#issue-2511546839) file summary in the prompt:
```
## File: 'src/file1.py'
### AI-generated file summary:
- edited function `func1` that does X
- Removed function `func2` that was not used
- ....
@@ ... @@ def func1():
__new hunk__
11 unchanged code line0 in the PR
12 unchanged code line1 in the PR
13 +new code line2 added in the PR
14 unchanged code line3 in the PR
__old hunk__
unchanged code line0
unchanged code line1
-old code line2 removed in the PR
unchanged code line3
@@ ... @@ def func2():
__new hunk__
...
__old hunk__
...
```
(3) The entire PR files that were retrieved are also used to expand and enhance the PR context (see [Dynamic Context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic-context/)).
(4) All the metadata described above represents several level of cumulative analysis - ranging from hunk level, to file level, to PR level, to organization level.
This comprehensive approach enables Qodo Merge AI models to generate more precise and contextually relevant suggestions and feedback.

View File

@ -0,0 +1,51 @@
## TL;DR
Qodo Merge implements a **self-reflection** process where the AI model reflects, scores, and re-ranks its own suggestions, eliminating irrelevant or incorrect ones.
This approach improves the quality and relevance of suggestions, saving users time and enhancing their experience.
Configuration options allow users to set a score threshold for further filtering out suggestions.
## Introduction - Efficient Review with Hierarchical Presentation
Given that not all generated code suggestions will be relevant, it is crucial to enable users to review them in a fast and efficient way, allowing quick identification and filtering of non-applicable ones.
To achieve this goal, Qodo Merge offers a dedicated hierarchical structure when presenting suggestions to users:
- A "category" section groups suggestions by their category, allowing users to quickly dismiss irrelevant suggestions.
- Each suggestion is first described by a one-line summary, which can be expanded to a full description by clicking on a collapsible.
- Upon expanding a suggestion, the user receives a more comprehensive description, and a code snippet demonstrating the recommendation.
!!! note "Fast Review"
This hierarchical structure is designed to facilitate rapid review of each suggestion, with users spending an average of ~5-10 seconds per item.
## Self-reflection and Re-ranking
The AI model is initially tasked with generating suggestions, and outputting them in order of importance.
However, in practice we observe that models often struggle to simultaneously generate high-quality code suggestions and rank them well in a single pass.
Furthermore, the initial set of generated suggestions sometimes contains easily identifiable errors.
To address these issues, we implemented a "self-reflection" process that refines suggestion ranking and eliminates irrelevant or incorrect proposals.
This process consists of the following steps:
1. Presenting the generated suggestions to the model in a follow-up call.
2. Instructing the model to score each suggestion on a scale of 0-10 and provide a rationale for the assigned score.
3. Utilizing these scores to re-rank the suggestions and filter out incorrect ones (with a score of 0).
4. Optionally, filtering out all suggestions below a user-defined score threshold.
Note that presenting all generated suggestions simultaneously provides the model with a comprehensive context, enabling it to make more informed decisions compared to evaluating each suggestion individually.
To conclude, the self-reflection process enables Qodo Merge to prioritize suggestions based on their importance, eliminate inaccurate or irrelevant proposals, and optionally exclude suggestions that fall below a specified threshold of significance.
This results in a more refined and valuable set of suggestions for the user, saving time and improving the overall experience.
## Example Results
![self_reflection](https://codium.ai/images/pr_agent/self_reflection1.png){width=768}
![self_reflection](https://codium.ai/images/pr_agent/self_reflection2.png){width=768}
## Appendix - Relevant Configuration Options
```
[pr_code_suggestions]
self_reflect_on_suggestions = true # Enable self-reflection on code suggestions
suggestions_score_threshold = 0 # Filter out suggestions with a score below this threshold (0-10)
```

View File

@ -0,0 +1,70 @@
## Overview - Static Code Analysis 💎
By combining static code analysis with LLM capabilities, Qodo Merge can provide a comprehensive analysis of the PR code changes on a component level.
It scans the PR code changes, finds all the code components (methods, functions, classes) that changed, and enables to interactively generate tests, docs, code suggestions and similar code search for each component.
!!! note "Language that are currently supported:"
Python, Java, C++, JavaScript, TypeScript, C#.
## Capabilities
### Analyze PR
The [`analyze`](https://qodo-merge-docs.qodo.ai/tools/analyze/) tool enables to interactively generate tests, docs, code suggestions and similar code search for each component that changed in the PR.
It can be invoked manually by commenting on any PR:
```
/analyze
```
An example result:
![Analyze 1](https://codium.ai/images/pr_agent/analyze_1.png){width=768}
Clicking on each checkbox will trigger the relevant tool for the selected component.
### Generate Tests
The [`test`](https://qodo-merge-docs.qodo.ai/tools/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, Or be triggered interactively by using the `analyze` tool.
![test1](https://codium.ai/images/pr_agent/test1.png){width=768}
### Generate Docs for a Component
The [`add_docs`](https://qodo-merge-docs.qodo.ai/tools/documentation/) tool scans the PR code changes, and automatically generate docstrings for any code components that changed in the PR.
It can be invoked manually by commenting on any PR:
```
/add_docs component_name
```
Or be triggered interactively by using the `analyze` tool.
![Docs single component](https://codium.ai/images/pr_agent/docs_single_component.png){width=768}
### Generate Code Suggestions for a Component
The [`improve_component`](https://qodo-merge-docs.qodo.ai/tools/improve_component/) tool generates code suggestions for a specific code component that changed in the PR.
It can be invoked manually by commenting on any PR:
```
/improve_component component_name
```
Or be triggered interactively by using the `analyze` tool.
![improve_component2](https://codium.ai/images/pr_agent/improve_component2.png){width=768}
### Find Similar Code
The [`similar code`](https://qodo-merge-docs.qodo.ai/tools/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`:
![similar code global](https://codium.ai/images/pr_agent/similar_code_global2.png){width=768}

View File

@ -4,16 +4,38 @@
--md-primary-fg-color: #765bfa; --md-primary-fg-color: #765bfa;
--md-accent-fg-color: #AEA1F1; --md-accent-fg-color: #AEA1F1;
} }
.md-nav__title, .md-nav__link {
font-size: 16px; .md-nav--primary {
.md-nav__link {
font-size: 18px; /* Change the font size as per your preference */
}
} }
.md-nav--primary {
position: relative; /* Ensure the element is positioned */
}
.md-nav--primary::before {
content: "";
position: absolute;
top: 0;
right: 10px; /* Move the border 10 pixels to the right */
width: 2px;
height: 100%;
background-color: #f5f5f5; /* Match the border color */
}
/*.md-nav__title, .md-nav__link {*/
/* font-size: 18px;*/
/* margin-top: 14px; !* Adjust the space as needed *!*/
/* margin-bottom: 14px; !* Adjust the space as needed *!*/
/*}*/
.md-tabs__link { .md-tabs__link {
font-size: 16px; font-size: 18px;
} }
.md-header__title { .md-header__title {
font-size: 20px; font-size: 20px;
margin-left: 0px !important; margin-left: 0px !important;
} }

67
docs/docs/faq/index.md Normal file
View File

@ -0,0 +1,67 @@
# FAQ
??? note "Question: Can Qodo Merge serve as a substitute for a human reviewer?"
#### Answer:<span style="display:none;">1</span>
Qodo Merge is designed to assist, not replace, human reviewers.
Reviewing PRs is a tedious and time-consuming task often seen as a "chore". In addition, the longer the PR the shorter the relative feedback, since long PRs can overwhelm reviewers, both in terms of technical difficulty, and the actual review time.
Qodo Merge aims to address these pain points, and to assist and empower both the PR author and reviewer.
However, Qodo Merge has built-in safeguards to ensure the developer remains in the driver's seat. For example:
1. Preserves user's original PR header
2. Places user's description above the AI-generated PR description
3. Cannot approve PRs; approval remains reviewer's responsibility
4. The code suggestions are optional, and aim to:
- Encourage self-review and self-reflection
- Highlight potential bugs or oversights
- Enhance code quality and promote best practices
Read more about this issue in our [blog](https://www.codium.ai/blog/understanding-the-challenges-and-pain-points-of-the-pull-request-cycle/)
___
??? note "Question: I received an incorrect or irrelevant suggestion. Why?"
#### Answer:<span style="display:none;">2</span>
- Modern AI models, like Claude 3.5 Sonnet and GPT-4, are improving rapidly but remain imperfect. Users should critically evaluate all suggestions rather than accepting them automatically.
- AI errors are rare, but possible. A main value from reviewing the code suggestions lies in their high probability of catching **mistakes or bugs made by the PR author**. We believe it's worth spending 30-60 seconds reviewing suggestions, even if some aren't relevant, as this practice can enhances code quality and prevent bugs in production.
- The hierarchical structure of the suggestions is designed to help the user to _quickly_ understand them, and to decide which ones are relevant and which are not:
- Only if the `Category` header is relevant, the user should move to the summarized suggestion description.
- Only if the summarized suggestion description is relevant, the user should click on the collapsible, to read the full suggestion description with a code preview example.
- In addition, we recommend to use the [`extra_instructions`](https://qodo-merge-docs.qodo.ai/tools/improve/#extra-instructions-and-best-practices) field to guide the model to suggestions that are more relevant to the specific needs of the project.
- The interactive [PR chat](https://qodo-merge-docs.qodo.ai/chrome-extension/) also provides an easy way to get more tailored suggestions and feedback from the AI model.
___
??? note "Question: How can I get more tailored suggestions?"
#### Answer:<span style="display:none;">3</span>
See [here](https://qodo-merge-docs.qodo.ai/tools/improve/#extra-instructions-and-best-practices) for more information on how to use the `extra_instructions` and `best_practices` configuration options, to guide the model to more tailored suggestions.
___
??? note "Question: Will you store my code ? Are you using my code to train models?"
#### Answer:<span style="display:none;">4</span>
No. Qodo Merge strict privacy policy ensures that your code is not stored or used for training purposes.
For a detailed overview of our data privacy policy, please refer to [this link](https://qodo-merge-docs.qodo.ai/overview/data_privacy/)
___
??? note "Question: Can I use my own LLM keys with Qodo Merge?"
#### Answer:<span style="display:none;">5</span>
When you self-host, you use your own keys.
Qodo Merge Pro with SaaS deployment is a hosted version of Qodo Merge, where Qodo manages the infrastructure and the keys.
For enterprise customers, on-prem deployment is also available. [Contact us](https://www.codium.ai/contact/#pricing) for more information.
___

View File

@ -0,0 +1,93 @@
# Qodo Merge Code Fine-tuning Benchmark
On coding tasks, the gap between open-source models and top closed-source models such as GPT4 is significant.
<br>
In practice, open-source models are unsuitable for most real-world code tasks, and require further fine-tuning to produce acceptable results.
_Qodo Merge fine-tuning benchmark_ aims to benchmark open-source models on their ability to be fine-tuned for a coding task.
Specifically, we chose to fine-tune open-source models on the task of analyzing a pull request, and providing useful feedback and code suggestions.
Here are the results:
<br>
<br>
**Model performance:**
| Model name | Model size [B] | Better than gpt-4 rate, after fine-tuning [%] |
|-----------------------------|----------------|----------------------------------------------|
| **DeepSeek 34B-instruct** | **34** | **40.7** |
| DeepSeek 34B-base | 34 | 38.2 |
| Phind-34b | 34 | 38 |
| Granite-34B | 34 | 37.6 |
| Codestral-22B-v0.1 | 22 | 32.7 |
| QWEN-1.5-32B | 32 | 29 |
| | | |
| **CodeQwen1.5-7B** | **7** | **35.4** |
| Llama-3.1-8B-Instruct | 8 | 35.2 |
| Granite-8b-code-instruct | 8 | 34.2 |
| CodeLlama-7b-hf | 7 | 31.8 |
| Gemma-7B | 7 | 27.2 |
| DeepSeek coder-7b-instruct | 7 | 26.8 |
| Llama-3-8B-Instruct | 8 | 26.8 |
| Mistral-7B-v0.1 | 7 | 16.1 |
<br>
**Fine-tuning impact:**
| Model name | Model size [B] | Fine-tuned | Better than gpt-4 rate [%] |
|---------------------------|----------------|------------|----------------------------|
| DeepSeek 34B-instruct | 34 | yes | 40.7 |
| DeepSeek 34B-instruct | 34 | no | 3.6 |
## Results analysis
- **Fine-tuning is a must** - without fine-tuning, open-source models provide poor results on most real-world code tasks, which include complicated prompt and lengthy context. We clearly see that without fine-tuning, deepseek model was 96.4% of the time inferior to GPT-4, while after fine-tuning, it is better 40.7% of the time.
- **Always start from a code-dedicated model** — When fine-tuning, always start from a code-dedicated model, and not from a general-usage model. The gaps in downstream results are very big.
- **Don't believe the hype** —newer models, or models from big-tech companies (Llama3, Gemma, Mistral), are not always better for fine-tuning.
- **The best large model** - For large 34B code-dedicated models, the gaps when doing proper fine-tuning are small. The current top model is **DeepSeek 34B-instruct**
- **The best small model** - For small 7B code-dedicated models, the gaps when fine-tuning are much larger. **CodeQWEN 1.5-7B** is by far the best model for fine-tuning.
- **Base vs. instruct** - For the top model (deepseek), we saw small advantage when starting from the instruct version. However, we recommend testing both versions on each specific task, as the base model is generally considered more suitable for fine-tuning.
## The dataset
### Training dataset
Our training dataset comprises 25,000 pull requests, aggregated from permissive license repos. For each pull request, we generated responses for the three main tools of Qodo Merge:
[Describe](https://qodo-merge-docs.qodo.ai/tools/describe/), [Review](https://qodo-merge-docs.qodo.ai/tools/improve/) and [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/).
On the raw data collected, we employed various automatic and manual cleaning techniques to ensure the outputs were of the highest quality, and suitable for instruct-tuning.
Here are the prompts, and example outputs, used as input-output pairs to fine-tune the models:
| Tool | Prompt | Example output |
|----------|------------------------------------------------------------------------------------------------------------|----------------|
| Describe | [link](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_description_prompts.toml) | [link](https://github.com/Codium-ai/pr-agent/pull/910#issue-2303989601) |
| Review | [link](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_reviewer_prompts.toml) | [link](https://github.com/Codium-ai/pr-agent/pull/910#issuecomment-2118761219) |
| Improve | [link](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_code_suggestions_prompts.toml) | [link](https://github.com/Codium-ai/pr-agent/pull/910#issuecomment-2118761309) |
### Evaluation dataset
- For each tool, we aggregated 100 additional examples to be used for evaluation. These examples were not used in the training dataset, and were manually selected to represent diverse real-world use-cases.
- For each test example, we generated two responses: one from the fine-tuned model, and one from the best code model in the world, `gpt-4-turbo-2024-04-09`.
- We used a third LLM to judge which response better answers the prompt, and will likely be perceived by a human as better response.
<br>
We experimented with three model as judges: `gpt-4-turbo-2024-04-09`, `gpt-4o`, and `claude-3-opus-20240229`. All three produced similar results, with the same ranking order. This strengthens the validity of our testing protocol.
The evaluation prompt can be found [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_evaluate_prompt_response.toml)
Here is an example of a judge model feedback:
```
command: improve
model1_score: 9,
model2_score: 6,
why: |
Response 1 is better because it provides more actionable and specific suggestions that directly
enhance the code's maintainability, performance, and best practices. For example, it suggests
using a variable for reusable widget instances and using named routes for navigation, which
are practical improvements. In contrast, Response 2 focuses more on general advice and less
actionable suggestions, such as changing variable names and adding comments, which are less
critical for immediate code improvement."
```

View File

@ -1,48 +1,61 @@
# Overview # Overview
CodiumAI PR-Agent is an open-source tool to help efficiently review and handle pull requests. Qodo Merge 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 [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 [Usage Guide](./usage-guide/index.md) for instructions on running the Qodo Merge 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. - See the [Tools Guide](./tools/index.md) for a detailed description of the different tools.
## PR-Agent Features ## Qodo Merge Docs Smart Search
PR-Agent offers extensive pull request functionalities across various git providers.
To search the documentation site using natural language:
1) Comment `/help "your question"` in either:
- A pull request where Qodo Merge is installed
- A [PR Chat](https://qodo-merge-docs.qodo.ai/chrome-extension/features/#pr-chat)
2) Qodo Merge will respond with an [answer](https://github.com/Codium-ai/pr-agent/pull/1241#issuecomment-2365259334) that includes relevant documentation links.
## Qodo Merge Features
Qodo Merge offers extensive pull request functionalities across various git providers.
| | | GitHub | Gitlab | Bitbucket | Azure DevOps | | | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|-------|-----------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:| |-------|-----------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|
| TOOLS | Review | ✅ | ✅ | ✅ | ✅ | | TOOLS | Review | ✅ | ✅ | ✅ | ✅ |
| | ⮑ Incremental | ✅ | | | | | | ⮑ Incremental | ✅ | | | |
| | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.ai/tools/review/#soc2-ticket-compliance){:target="_blank"} 💎 | ✅ | ✅ | ✅ | | | | ⮑ [SOC2 Compliance](https://qodo-merge-docs.qodo.ai/tools/review/#soc2-ticket-compliance){:target="_blank"} 💎 | ✅ | ✅ | ✅ | |
| | Ask | ✅ | ✅ | ✅ | ✅ | | | Ask | ✅ | ✅ | ✅ | ✅ |
| | Describe | ✅ | ✅ | ✅ | ✅ | | | Describe | ✅ | ✅ | ✅ | ✅ |
| | ⮑ [Inline file summary](https://pr-agent-docs.codium.ai/tools/describe/#inline-file-summary){:target="_blank"} 💎 | ✅ | ✅ | | | | | ⮑ [Inline file summary](https://qodo-merge-docs.qodo.ai/tools/describe/#inline-file-summary){:target="_blank"} 💎 | ✅ | ✅ | | |
| | Improve | ✅ | ✅ | ✅ | ✅ | | | Improve | ✅ | ✅ | ✅ | ✅ |
| | ⮑ Extended | ✅ | ✅ | ✅ | ✅ | | | ⮑ Extended | ✅ | ✅ | ✅ | ✅ |
| | [Custom Prompt](./tools/custom_prompt.md){:target="_blank"} 💎 | ✅ | ✅ | ✅ | | | | [Custom Prompt](./tools/custom_prompt.md){:target="_blank"} 💎 | ✅ | ✅ | ✅ | |
| | Reflect and Review | ✅ | ✅ | ✅ | | | | Reflect and Review | ✅ | ✅ | ✅ | |
| | Update CHANGELOG.md | ✅ | ✅ | ✅ | | | | Update CHANGELOG.md | ✅ | ✅ | ✅ | |
| | Find Similar Issue | ✅ | | | | | | Find Similar Issue | ✅ | | | |
| | [Add PR Documentation](./tools/documentation.md){:target="_blank"} 💎 | ✅ | ✅ | | | | | [Add PR Documentation](./tools/documentation.md){:target="_blank"} 💎 | ✅ | ✅ | | |
| | [Generate Custom Labels](./tools/describe.md#handle-custom-labels-from-the-repos-labels-page-💎){: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"} 💎 | ✅ | ✅ | | | | | [Analyze PR Components](./tools/analyze.md){:target="_blank"} 💎 | ✅ | ✅ | | |
| | | | | | | | | | | | | |
| USAGE | CLI | ✅ | ✅ | ✅ | ✅ | | USAGE | CLI | ✅ | ✅ | ✅ | ✅ |
| | App / webhook | ✅ | ✅ | ✅ | ✅ | | | App / webhook | ✅ | ✅ | ✅ | ✅ |
| | Actions | ✅ | | | | | | Actions | ✅ | | | |
| | | | | | | | | | | |
| CORE | PR compression | ✅ | ✅ | ✅ | ✅ | | CORE | PR compression | ✅ | ✅ | ✅ | ✅ |
| | Repo language prioritization | ✅ | ✅ | ✅ | ✅ | | | Repo language prioritization | ✅ | ✅ | ✅ | ✅ |
| | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | ✅ | | | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | ✅ |
| | Multiple models support | ✅ | ✅ | ✅ | ✅ | | | Multiple models support | ✅ | ✅ | ✅ | ✅ |
| | Incremental PR review | ✅ | | | | | | Incremental PR review | ✅ | | | |
| | [Static code analysis](./tools/analyze.md/){:target="_blank"} 💎 | ✅ | ✅ | ✅ | | | | [Static code analysis](./tools/analyze.md/){:target="_blank"} 💎 | ✅ | ✅ | ✅ | |
| | [Multiple configuration options](./usage-guide/configuration_options.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"} 💎 marks a feature available only in [Qodo Merge Pro](https://www.codium.ai/pricing/){:target="_blank"}
## Example Results ## Example Results
@ -74,51 +87,8 @@ PR-Agent offers extensive pull request functionalities across various git provid
## How it Works ## How it Works
The following diagram illustrates PR-Agent tools and their flow: The following diagram illustrates Qodo Merge tools and their flow:
![PR-Agent Tools](https://codium.ai/images/pr_agent/diagram-v0.9.png) ![Qodo Merge 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 Check out the [core abilities](core-abilities/index.md) page for a comprehensive overview of the variety of core abilities used by Qodo Merge.
## 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\GitLab\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:
- (Tool): [**Analyze PR components**](./tools/analyze.md/)
- (Tool): [**Custom Prompt Suggestions**](./tools/custom_prompt.md/)
- (Tool): [**Tests**](./tools/test.md/)
- (Tool): [**PR documentation**](./tools/documentation.md/)
- (Tool): [**Improve Component**](https://pr-agent-docs.codium.ai/tools/improve_component/)
- (Tool): [**Similar code search**](https://pr-agent-docs.codium.ai/tools/similar_code/)
- (Tool): [**CI feedback**](./tools/ci_feedback.md/)
- (Feature): [**Interactive triggering**](./usage-guide/automations_and_usage.md/#interactive-triggering)
- (Feature): [**SOC2 compliance check**](./tools/review.md/#soc2-ticket-compliance)
- (Feature): [**Custom labels**](./tools/describe.md/#handle-custom-labels-from-the-repos-labels-page)
- (Feature): [**Global and wiki configuration**](./usage-guide/configuration_options.md/#wiki-configuration-file)
- (Feature): [**Inline file summary**](https://pr-agent-docs.codium.ai/tools/describe/#inline-file-summary)
## Data Privacy
### Self-hosted PR-Agent
- 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
### CodiumAI-hosted PR-Agent Pro 💎
- 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.
- For certain clients, CodiumAI-hosted PR-Agent Pro will use CodiumAIs proprietary models — if this is the case, you will be notified.
- No passive collection of Code and Pull Requests data — PR-Agent will be active only when you invoke it, and it will then extract and analyze only data relevant to the executed command and queried pull request.
### PR-Agent Chrome extension
- The [PR-Agent Chrome extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) serves solely to modify the visual appearance of a GitHub PR screen. It does not transmit any user's repo or pull request code. Code is only sent for processing when a user submits a GitHub comment that activates a PR-Agent tool, in accordance with the standard privacy policy of PR-Agent.

View File

@ -1,4 +1,62 @@
## Azure DevOps provider ## Azure DevOps Pipeline
You can use a pre-built Action Docker image to run Qodo Merge as an Azure devops pipeline.
add the following file to your repository under `azure-pipelines.yml`:
```yaml
# Opt out of CI triggers
trigger: none
# Configure PR trigger
pr:
branches:
include:
- '*'
autoCancel: true
drafts: false
stages:
- stage: pr_agent
displayName: 'PR Agent Stage'
jobs:
- job: pr_agent_job
displayName: 'PR Agent Job'
pool:
vmImage: 'ubuntu-latest'
container:
image: codiumai/pr-agent:latest
options: --entrypoint ""
variables:
- group: pr_agent
steps:
- script: |
echo "Running PR Agent action step"
# Construct PR_URL
PR_URL="${SYSTEM_COLLECTIONURI}${SYSTEM_TEAMPROJECT}/_git/${BUILD_REPOSITORY_NAME}/pullrequest/${SYSTEM_PULLREQUEST_PULLREQUESTID}"
echo "PR_URL=$PR_URL"
# Extract organization URL from System.CollectionUri
ORG_URL=$(echo "$(System.CollectionUri)" | sed 's/\/$//') # Remove trailing slash if present
echo "Organization URL: $ORG_URL"
export azure_devops__org="$ORG_URL"
export config__git_provider="azure"
pr-agent --pr_url="$PR_URL" describe
pr-agent --pr_url="$PR_URL" review
pr-agent --pr_url="$PR_URL" improve
env:
azure_devops__pat: $(azure_devops_pat)
openai__key: $(OPENAI_KEY)
displayName: 'Run Qodo Merge'
```
This script will run Qodo Merge on every new merge request, with the `improve`, `review`, and `describe` commands.
Note that you need to export the `azure_devops__pat` and `OPENAI_KEY` variables in the Azure DevOps pipeline settings (Pipelines -> Library -> + Variable group):
![Qodo Merge Pro](https://codium.ai/images/pr_agent/azure_devops_pipeline_secrets.png){width=468}
Make sure to give pipeline permissions to the `pr_agent` variable group.
## Azure DevOps from CLI
To use Azure DevOps provider use the following settings in configuration.toml: To use Azure DevOps provider use the following settings in configuration.toml:
``` ```

View File

@ -1,7 +1,7 @@
## Run as a Bitbucket Pipeline ## Run as a Bitbucket Pipeline
You can use the Bitbucket Pipeline system to run PR-Agent on every pull request open or update. You can use the Bitbucket Pipeline system to run Qodo Merge on every pull request open or update.
1. Add the following file in your repository bitbucket_pipelines.yml 1. Add the following file in your repository bitbucket_pipelines.yml
@ -27,9 +27,9 @@ You can get a Bitbucket token for your repository by following Repository Settin
Note that comments on a PR are not supported in Bitbucket Pipeline. Note that comments on a PR are not supported in Bitbucket Pipeline.
## Run using CodiumAI-hosted Bitbucket app ## Run using CodiumAI-hosted Bitbucket app 💎
Please contact [support@codium.ai](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. Please contact visit [Qodo Merge Pro](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 ## Bitbucket Server and Data Center
@ -58,7 +58,7 @@ python cli.py --pr_url https://git.onpreminstanceofbitbucket.com/projects/PROJEC
### Run it as service ### Run it as service
To run pr-agent as webhook, build the docker image: To run Qodo Merge as webhook, build the docker image:
``` ```
docker build . -t codiumai/pr-agent:bitbucket_server_webhook --target bitbucket_server_webhook -f docker/Dockerfile 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 docker push codiumai/pr-agent:bitbucket_server_webhook # Push to your Docker repository

View File

@ -1,6 +1,6 @@
## Run as a GitHub Action ## Run as a GitHub Action
You can use our pre-built Github Action Docker image to run PR-Agent as a Github Action. You can use our pre-built Github Action Docker image to run Qodo Merge as a Github Action.
1) Add the following file to your repository under `.github/workflows/pr_agent.yml`: 1) Add the following file to your repository under `.github/workflows/pr_agent.yml`:
@ -26,15 +26,28 @@ jobs:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
** if you want to pin your action to a specific release (v2.0 for example) for stability reasons, use:
if you want to pin your action to a specific release (v0.23 for example) for stability reasons, use:
```yaml ```yaml
... ...
steps: steps:
- name: PR Agent action step - name: PR Agent action step
id: pragent id: pragent
uses: Codium-ai/pr-agent@v2.0 uses: docker://codiumai/pr-agent:0.23-github_action
... ...
``` ```
For enhanced security, you can also specify the Docker image by its [digest](https://hub.docker.com/repository/docker/codiumai/pr-agent/tags):
```yaml
...
steps:
- name: PR Agent action step
id: pragent
uses: docker://codiumai/pr-agent@sha256:14165e525678ace7d9b51cda8652c2d74abb4e1d76b57c4a6ccaeba84663cc64
...
```
2) Add the following secret to your repository under `Settings > Secrets and variables > Actions > New repository secret > Add secret`: 2) Add the following secret to your repository under `Settings > Secrets and variables > Actions > New repository secret > Add secret`:
``` ```
@ -47,7 +60,7 @@ The GITHUB_TOKEN secret is automatically created by GitHub.
3) Merge this change to your main branch. 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. 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: 4) You may configure Qodo Merge 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 ```yaml
env: env:
# ... previous environment values # ... previous environment values
@ -55,7 +68,7 @@ When you open your next PR, you should see a comment from `github-actions` bot w
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" # Disable tests review PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" # Disable tests review
PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions
``` ```
See detailed usage instructions in the [USAGE GUIDE](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#github-action) See detailed usage instructions in the [USAGE GUIDE](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-action)
--- ---
@ -142,7 +155,7 @@ cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
9. Install the app by navigating to the "Install App" tab and selecting your desired repositories. 9. Install the app by navigating to the "Install App" tab and selecting your desired repositories.
> **Note:** When running PR-Agent from GitHub App, the default configuration file (configuration.toml) will be loaded. > **Note:** When running Qodo Merge 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` > 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) > For more information please check out the [USAGE GUIDE](../usage-guide/automations_and_usage.md#github-app)
--- ---
@ -172,7 +185,7 @@ For example: `GITHUB.WEBHOOK_SECRET` --> `GITHUB__WEBHOOK_SECRET`
## AWS CodeCommit Setup ## AWS CodeCommit Setup
Not all features have been added to CodeCommit yet. As of right now, CodeCommit has been implemented to run the pr-agent CLI on the command line, using AWS credentials stored in environment variables. (More features will be added in the future.) The following is a set of instructions to have pr-agent do a review of your CodeCommit pull request from the command line: Not all features have been added to CodeCommit yet. As of right now, CodeCommit has been implemented to run the Qodo Merge 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 Qodo Merge 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 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 * Note: That user should have CLI access only, not Console access

View File

@ -1,3 +1,45 @@
## Run as a GitLab Pipeline
You can use a pre-built Action Docker image to run Qodo Merge as a GitLab pipeline. This is a simple way to get started with Qodo Merge without setting up your own server.
(1) Add the following file to your repository under `.gitlab-ci.yml`:
```yaml
stages:
- pr_agent
pr_agent_job:
stage: pr_agent
image:
name: codiumai/pr-agent:latest
entrypoint: [""]
script:
- cd /app
- echo "Running PR Agent action step"
- export MR_URL="$CI_MERGE_REQUEST_PROJECT_URL/merge_requests/$CI_MERGE_REQUEST_IID"
- echo "MR_URL=$MR_URL"
- export gitlab__url=$CI_SERVER_PROTOCOL://$CI_SERVER_FQDN
- export gitlab__PERSONAL_ACCESS_TOKEN=$GITLAB_PERSONAL_ACCESS_TOKEN
- export config__git_provider="gitlab"
- export openai__key=$OPENAI_KEY
- python -m pr_agent.cli --pr_url="$MR_URL" describe
- python -m pr_agent.cli --pr_url="$MR_URL" review
- python -m pr_agent.cli --pr_url="$MR_URL" improve
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
```
This script will run Qodo Merge on every new merge request. You can modify the `rules` section to run Qodo Merge on different events.
You can also modify the `script` section to run different Qodo Merge commands, or with different parameters by exporting different environment variables.
(2) Add the following masked variables to your GitLab repository (CI/CD -> Variables):
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token.
- `OPENAI_KEY`: Your OpenAI key.
Note that if your base branches are not protected, don't set the variables as `protected`, since the pipeline will not have access to them.
## Run a GitLab webhook server ## Run a GitLab webhook server
1. From the GitLab workspace or group, create an access token. Enable the "api" scope only. 1. From the GitLab workspace or group, create an access token. Enable the "api" scope only.
@ -7,14 +49,14 @@
``` ```
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))") 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. 3. Follow the instructions to build the Docker image, setup a secrets file and deploy on your own server from [here](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-app) steps 4-7.
4. In the secrets file, fill in the following: 4. In the secrets file, fill in the following:
- Your OpenAI key. - 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. - 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) - 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. 5. Create a webhook in GitLab. Set the URL to ```http[s]://<PR_AGENT_HOSTNAME>/webhook```. Set the secret token to the generated secret from step 2.
In the "Trigger" section, check the comments and merge request events boxes. 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. 6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands.

View File

@ -1,12 +1,12 @@
# Installation # Installation
## Self-hosted PR-Agent ## Self-hosted Qodo Merge
If you choose to host you own PR-Agent, you first need to acquire two tokens: If you choose to host you own Qodo Merge, 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). 1. An OpenAI key from [here](https://platform.openai.com/api-keys), with access to GPT-4 (or a key for other [language models](https://qodo-merge-docs.qodo.ai/usage-guide/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)] 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: There are several ways to use self-hosted Qodo Merge:
- [Locally](./locally.md) - [Locally](./locally.md)
- [GitHub](./github.md) - [GitHub](./github.md)
@ -14,9 +14,8 @@ There are several ways to use self-hosted PR-Agent:
- [BitBucket](./bitbucket.md) - [BitBucket](./bitbucket.md)
- [Azure DevOps](./azure.md) - [Azure DevOps](./azure.md)
## PR-Agent Pro 💎 ## Qodo Merge Pro 💎
PR-Agent Pro, an app for GitHub\GitLab\BitBucket hosted by CodiumAI, is also available. Qodo Merge Pro, an app hosted by CodiumAI for GitHub\GitLab\BitBucket, is also available.
<br> <br>
With PR-Agent Pro Installation is as simple as signing up and adding the PR-Agent app to your relevant repo. With Qodo Merge Pro, installation is as simple as signing up and adding the Qodo Merge app to your relevant repo.
<br> See [here](https://qodo-merge-docs.qodo.ai/installation/pr_agent_pro/) for more details.
See [here](./pr_agent_pro.md) for more details.

View File

@ -1,29 +1,33 @@
## Getting Started with PR-Agent Pro ## Getting Started with Qodo Merge Pro
PR-Agent Pro is a versatile application compatible with GitHub, GitLab, and BitBucket, hosted by CodiumAI. Qodo Merge Pro is a versatile application compatible with GitHub, GitLab, and BitBucket, hosted by CodiumAI.
See [here](https://pr-agent-docs.codium.ai/#pr-agent-pro) for more details about the benefits of using PR-Agent Pro. See [here](https://qodo-merge-docs.qodo.ai/overview/pr_agent_pro/) for more details about the benefits of using Qodo Merge Pro.
Interested parties can subscribe to PR-Agent Pro through the following [link](https://www.codium.ai/pricing/). Interested parties can subscribe to Qodo Merge Pro through the following [link](https://www.codium.ai/pricing/).
After subscribing, you are granted the ability to easily install the application across any of your repositories. After subscribing, you are granted the ability to easily install the application across any of your repositories.
![PR Agent Pro](https://codium.ai/images/pr_agent/pr_agent_pro_install.png){width=468} ![Qodo Merge Pro](https://codium.ai/images/pr_agent/pr_agent_pro_install.png){width=468}
Each user who wants to use PR-Agent pro needs to buy a seat. Each user who wants to use Qodo Merge pro needs to buy a seat.
Initially, CodiumAI offers a two-week trial period at no cost, after which continued access requires each user to secure a personal seat. Initially, CodiumAI offers a two-week trial period at no cost, after which continued access requires each user to secure a personal seat.
Once a user acquires a seat, they gain the flexibility to use PR-Agent Pro across any repository where it was enabled. Once a user acquires a seat, they gain the flexibility to use Qodo Merge Pro across any repository where it was enabled.
Users without a purchased seat who interact with a repository featuring PR-Agent Pro are entitled to receive up to five complimentary feedbacks. Users without a purchased seat who interact with a repository featuring Qodo Merge Pro are entitled to receive up to five complimentary feedbacks.
Beyond this limit, PR-Agent Pro will cease to respond to their inquiries unless a seat is purchased. Beyond this limit, Qodo Merge Pro will cease to respond to their inquiries unless a seat is purchased.
## Install Qodo Merge Pro for GitHub Enterprise Server
You can install Qodo Merge Pro application on your GitHub Enterprise Server, and enjoy two weeks of free trial.
After the trial period, to continue using Qodo Merge Pro, you will need to contact us for an [Enterprise license](https://www.codium.ai/pricing/).
## Install PR-Agent Pro for GitLab (Teams & Enterprise) ## Install Qodo Merge Pro for GitLab (Teams & Enterprise)
Since GitLab platform does not support apps, installing PR-Agent Pro for GitLab is a bit more involved, and requires the following steps: Since GitLab platform does not support apps, installing Qodo Merge Pro for GitLab is a bit more involved, and requires the following steps:
### Step 1 ### Step 1
Acquire a personal, project or group level access token. Enable the “api” scope in order to allow PR-Agent to read pull requests, comment and respond to requests. Acquire a personal, project or group level access token. Enable the “api” scope in order to allow Qodo Merge to read pull requests, comment and respond to requests.
<figure markdown="1"> <figure markdown="1">
![Step 1](https://www.codium.ai/images/pr_agent/gitlab_pro_pat.png){width=750} ![Step 1](https://www.codium.ai/images/pr_agent/gitlab_pro_pat.png){width=750}
@ -61,4 +65,4 @@ Enable SSL verification: Check the box.
Youre all set! Youre all set!
Open a new merge request or add a MR comment with one of PR-Agents commands such as /review, /describe or /improve. Open a new merge request or add a MR comment with one of Qodo Merges commands such as /review, /describe or /improve.

View File

@ -0,0 +1,16 @@
## Self-hosted Qodo Merge
- If you self-host Qodo Merge with your OpenAI (or other LLM provider) API key, it is between you and the provider. We don't send your code data to Qodo Merge servers.
## Qodo Merge Pro 💎
- When using Qodo Merge 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.
- For certain clients, CodiumAI-hosted Qodo Merge Pro will use CodiumAIs proprietary models. If this is the case, you will be notified.
- No passive collection of Code and Pull Requests data — Qodo Merge will be active only when you invoke it, and it will then extract and analyze only data relevant to the executed command and queried pull request.
## Qodo Merge Chrome extension
- The [Qodo Merge Chrome extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) will not send your code to any external servers.

View File

@ -0,0 +1,94 @@
# Overview
Qodo Merge 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 Qodo Merge 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.
## Qodo Merge Docs Smart Search
To search the documentation site using natural language:
1) Comment `/help "your question"` in either:
- A pull request where Qodo Merge is installed
- A [PR Chat](https://qodo-merge-docs.qodo.ai/chrome-extension/features/#pr-chat)
2) Qodo Merge will respond with an [answer](https://github.com/Codium-ai/pr-agent/pull/1241#issuecomment-2365259334) that includes relevant documentation links.
## Qodo Merge Features
Qodo Merge offers extensive pull request functionalities across various git providers.
| | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|-------|-----------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|
| TOOLS | Review | ✅ | ✅ | ✅ | ✅ |
| | ⮑ Incremental | ✅ | | | |
| | ⮑ [SOC2 Compliance](https://qodo-merge-docs.qodo.ai/tools/review/#soc2-ticket-compliance){:target="_blank"} 💎 | ✅ | ✅ | ✅ | ✅ |
| | Ask | ✅ | ✅ | ✅ | ✅ |
| | Describe | ✅ | ✅ | ✅ | ✅ |
| | ⮑ [Inline file summary](https://qodo-merge-docs.qodo.ai/tools/describe/#inline-file-summary){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
| | Improve | ✅ | ✅ | ✅ | ✅ |
| | ⮑ Extended | ✅ | ✅ | ✅ | ✅ |
| | [Custom Prompt](./tools/custom_prompt.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 | ✅ | ✅ | ✅ | ✅ |
| | 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 [Qodo Merge Pro](https://www.codium.ai/pricing/){:target="_blank"}
## Example Results
<hr>
#### [/describe](https://github.com/Codium-ai/pr-agent/pull/530)
<figure markdown="1">
![/describe](https://www.codium.ai/images/pr_agent/describe_new_short_main.png){width=512}
</figure>
<hr>
#### [/review](https://github.com/Codium-ai/pr-agent/pull/732#issuecomment-1975099151)
<figure markdown="1">
![/review](https://www.codium.ai/images/pr_agent/review_new_short_main.png){width=512}
</figure>
<hr>
#### [/improve](https://github.com/Codium-ai/pr-agent/pull/732#issuecomment-1975099159)
<figure markdown="1">
![/improve](https://www.codium.ai/images/pr_agent/improve_new_short_main.png){width=512}
</figure>
<hr>
#### [/generate_labels](https://github.com/Codium-ai/pr-agent/pull/530)
<figure markdown="1">
![/generate_labels](https://www.codium.ai/images/pr_agent/geneare_custom_labels_main_short.png){width=300}
</figure>
<hr>
## How it Works
The following diagram illustrates Qodo Merge tools and their flow:
![Qodo Merge 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

View File

@ -0,0 +1,52 @@
### Overview
[Qodo Merge Pro](https://www.codium.ai/pricing/) is a hosted version of Qodo Merge, provided by Qodo. A complimentary two-week trial is offered, followed by a monthly subscription fee.
Qodo Merge Pro is designed for companies and teams that require additional features and capabilities. It 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 Qodo Merge app to your GitHub\GitLab\BitBucket repo.
2. **Improved privacy** - No data will be stored or used to train models. Qodo Merge Pro will employ zero data retention, and will use an OpenAI and Claude accounts with zero data retention.
3. **Improved support** - Qodo Merge Pro users will receive priority support, and will be able to request new features and capabilities.
4. **Supporting self-hosted git servers** - Qodo Merge Pro can be installed on GitHub Enterprise Server, GitLab, and BitBucket. For more information, see the [installation guide](https://qodo-merge-docs.qodo.ai/installation/pr_agent_pro/).
5. **PR Chat** - Qodo Merge Pro allows you to engage in [private chat](https://qodo-merge-docs.qodo.ai/chrome-extension/features/#pr-chat) about your pull requests on private repositories.
### Additional features
Here are some of the additional features and capabilities that Qodo Merge Pro offers:
| Feature | Description |
|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [**Model selection**](https://qodo-merge-docs.qodo.ai/usage-guide/PR_agent_pro_models/) | Choose the model that best fits your needs, among top models like `GPT4` and `Claude-Sonnet-3.5`
| [**Global and wiki configuration**](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/) | Control configurations for many repositories from a single location; <br>Edit configuration of a single repo without commiting code |
| [**Apply suggestions**](https://qodo-merge-docs.qodo.ai/tools/improve/#overview) | Generate commitable code from the relevant suggestions interactively by clicking on a checkbox |
| [**Suggestions impact**](https://qodo-merge-docs.qodo.ai/tools/improve/#assessing-impact) | Automatically mark suggestions that were implemented by the user (either directly in GitHub, or indirectly in the IDE) to enable tracking of the impact of the suggestions |
| [**CI feedback**](https://qodo-merge-docs.qodo.ai/tools/ci_feedback/) | Automatically analyze failed CI checks on GitHub and provide actionable feedback in the PR conversation, helping to resolve issues quickly |
| [**Advanced usage statistics**](https://www.codium.ai/contact/#/) | Qodo Merge Pro offers detailed statistics at user, repository, and company levels, including metrics about Qodo Merge usage, and also general statistics and insights |
| [**Incorporating companies' best practices**](https://qodo-merge-docs.qodo.ai/tools/improve/#best-practices) | Use the companies' best practices as reference to increase the effectiveness and the relevance of the code suggestions |
| [**Interactive triggering**](https://qodo-merge-docs.qodo.ai/tools/analyze/#example-usage) | Interactively apply different tools via the `analyze` command |
| [**SOC2 compliance check**](https://qodo-merge-docs.qodo.ai/tools/review/#configuration-options) | Ensures the PR contains a ticket to a project management system (e.g., Jira, Asana, Trello, etc.)
| [**Custom labels**](https://qodo-merge-docs.qodo.ai/tools/describe/#handle-custom-labels-from-the-repos-labels-page) | Define custom labels for Qodo Merge to assign to the PR |
### Additional tools
Here are additional tools that are available only for Qodo Merge Pro users:
| Feature | Description |
|---------|-------------|
| [**Custom Prompt Suggestions**](https://qodo-merge-docs.qodo.ai/tools/custom_prompt/) | Generate code suggestions based on custom prompts from the user |
| [**Analyze PR components**](https://qodo-merge-docs.qodo.ai/tools/analyze/) | Identify the components that changed in the PR, and enable to interactively apply different tools to them |
| [**Tests**](https://qodo-merge-docs.qodo.ai/tools/test/) | Generate tests for code components that changed in the PR |
| [**PR documentation**](https://qodo-merge-docs.qodo.ai/tools/documentation/) | Generate docstring for code components that changed in the PR |
| [**Improve Component**](https://qodo-merge-docs.qodo.ai/tools/improve_component/) | Generate code suggestions for code components that changed in the PR |
| [**Similar code search**](https://qodo-merge-docs.qodo.ai/tools/similar_code/) | Search for similar code in the repository, organization, or entire GitHub |
### Supported languages
Qodo Merge Pro leverages the world's leading code models - Claude 3.5 Sonnet and GPT-4.
As a result, its primary tools such as `describe`, `review`, and `improve`, as well as the PR-chat feature, support virtually all programming languages.
For specialized commands that require static code analysis, Qodo Merge Pro offers support for specific languages. For more details about features that require static code analysis, please refer to the [documentation](https://qodo-merge-docs.qodo.ai/tools/analyze/#overview).

View File

@ -1,7 +1,7 @@
## Overview ## Overview
The `analyze` tool combines advanced static code analysis with LLM capabilities to provide a comprehensive analysis of the PR code changes. The `analyze` tool combines advanced 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 enables to interactively generate tests, docs, code suggestions and similar code search for each component. The tool scans the PR code changes, finds the code components (methods, functions, classes) that changed, and enables to interactively generate tests, docs, code suggestions and similar code search for each component.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```

View File

@ -28,7 +28,7 @@ When working from CLI, you need to apply the [configuration changes](#configurat
To enable custom labels, you need to apply the [configuration changes](#configuration-options) to the local `.pr_agent.toml` file in you repository. 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 💎 #### 3. Handle custom labels from the Repo's labels page 💎
> This feature is available only in PR-Agent Pro > This feature is available only in Qodo Merge Pro
* GitHub : `https://github.com/{owner}/{repo}/labels`, or click on the "Labels" tab in the issues or PRs page. * 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. * GitLab : `https://gitlab.com/{owner}/{repo}/-/labels`, or click on "Manage" -> "Labels" on the left menu.

View File

@ -25,7 +25,7 @@ If you want to edit [configurations](#configuration-options), add the relevant o
### Automatic triggering ### Automatic triggering
To run the `describe` automatically when a PR is opened, define in a [configuration file](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/#wiki-configuration-file): To run the `describe` automatically when a PR is opened, define in a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#wiki-configuration-file):
``` ```
[github_app] [github_app]
pr_commands = [ pr_commands = [
@ -77,7 +77,7 @@ publish_labels = ...
</tr> </tr>
<tr> <tr>
<td><b>final_update_message</b></td> <td><b>final_update_message</b></td>
<td>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.</td> <td>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 false.</td>
</tr> </tr>
<tr> <tr>
<td><b>enable_semantic_files_types</b></td> <td><b>enable_semantic_files_types</b></td>
@ -87,6 +87,10 @@ publish_labels = ...
<td><b>collapsible_file_list</b></td> <td><b>collapsible_file_list</b></td>
<td>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".</td> <td>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".</td>
</tr> </tr>
<tr>
<td><b>enable_large_pr_handling</b></td>
<td>Pro feature. If set to true, in case of a large PR the tool will make several calls to the AI and combine them to be able to cover more files. Default is true.</td>
</tr>
<tr> <tr>
<td><b>enable_help_text</b></td> <td><b>enable_help_text</b></td>
<td>If set to true, the tool will display a help text in the comment. Default is false.</td> <td>If set to true, the tool will display a help text in the comment. Default is false.</td>
@ -108,7 +112,7 @@ If you prefer to have the file summaries appear in the "Files changed" tab on ev
![Diffview table](https://codium.ai/images/pr_agent/diffview-table.png){width=512} ![Diffview table](https://codium.ai/images/pr_agent/diffview-table.png){width=512}
- `true`: A collapsable file comment with changes title and a changes summary for each file in the PR. - `true`: A collapsible file comment with changes title and a changes summary for each file in the PR.
![Diffview changes](https://codium.ai/images/pr_agent/diffview_changes.png){width=512} ![Diffview changes](https://codium.ai/images/pr_agent/diffview_changes.png){width=512}
@ -150,30 +154,33 @@ The marker `pr_agent:type` will be replaced with the PR type, `pr_agent:summary`
- `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. - `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.
## Custom labels ## 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`]. 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`].
You can define custom labels that are relevant for your repo and use cases. You can define custom labels that are relevant for your repo and use cases.
Custom labels can be defined in a [configuration file](https://pr-agent-docs.codium.ai/tools/custom_labels/#configuration-options), or directly in the repo's [labels page](#handle-custom-labels-from-the-repos-labels-page). Custom labels can be defined in a [configuration file](https://qodo-merge-docs.qodo.ai/tools/custom_labels/#configuration-options), or directly in the repo's [labels page](#handle-custom-labels-from-the-repos-labels-page).
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. 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.
Each label description should be a **conditional statement**, that indicates if to add the label to the PR or not, according to the PR content. Each label description should be a **conditional statement**, that indicates if to add the label to the PR or not, according to the PR content.
### Handle custom labels from a configuration file
Example for a custom labels configuration setup in a configuration file:
```
[config]
enable_custom_labels=true
[custom_labels."sql_changes"]
description = "Use when a PR contains changes to SQL queries"
[custom_labels."test"]
description = "use when a PR primarily contains new tests"
...
```
### Handle custom labels from the Repo's labels page 💎 ### Handle custom labels from the Repo's labels page 💎
You can control the custom labels that will be suggested by the `describe` tool, from the repo's labels page: You can also 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) * 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) * GitLab : go to `https://gitlab.com/{owner}/{repo}/-/labels` (or click on "Manage" -> "Labels" on the left menu)
@ -183,6 +190,14 @@ Now add/edit the custom labels. they should be formatted as follows:
* Label name: The name of the custom label. * 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> * Description: Start the description of with prefix `pr_agent:`, for example: `pr_agent: Description of when AI should suggest this label`.<br>
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 description should be comprehensive and detailed, indicating when to add the desired label. For example: The description should be comprehensive and detailed, indicating when to add the desired label. For example:
![Add native custom labels](https://codium.ai/images/pr_agent/add_native_custom_labels.png){width=768} ![Add native custom labels](https://codium.ai/images/pr_agent/add_native_custom_labels.png){width=768}
@ -190,11 +205,11 @@ The description should be comprehensive and detailed, indicating when to add the
## Usage Tips ## Usage Tips
!!! tip "Automation" !!! tip "Automation"
- When you first install PR-Agent app, the [default mode](../usage-guide/automations_and_usage.md#github-app) for the describe tool is: - When you first install Qodo Merge app, the [default mode](../usage-guide/automations_and_usage.md#github-app) for the describe tool is:
``` ```
pr_commands = ["/describe", ...] pr_commands = ["/describe", ...]
``` ```
meaning the `describe` tool will run automatically on every PR, with the default configurations. meaning the `describe` tool will run automatically on every PR, with the default configurations.
- Markers are an alternative way to control the generated description, to give maximal control to the user. If you set: - Markers are an alternative way to control the generated description, to give maximal control to the user. If you set:

View File

@ -1,6 +1,6 @@
## Overview ## Overview
The `help` tool provides a list of all the available tools and their descriptions. The `help` tool provides a list of all the available tools and their descriptions.
For PR-Agent Pro users, it also enables to trigger each tool by checking the relevant box. For Qodo Merge Pro users, it also enables to trigger each tool by checking the relevant box.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```

View File

@ -1,38 +1,43 @@
## Overview ## Overview
The `improve` tool scans the PR code changes, and automatically generates suggestions for improving the PR code. The `improve` tool scans the PR code changes, and automatically generates [meaningful](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_code_suggestions_prompts.toml#L41) 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: 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:
``` ```toml
/improve /improve
``` ```
![code_suggestions_as_comment_closed.png](https://codium.ai/images/pr_agent/code_suggestions_as_comment_closed.png){width=512}
![code_suggestions_as_comment_open.png](https://codium.ai/images/pr_agent/code_suggestions_as_comment_open.png){width=512}
Note that the `Apply this suggestion` checkbox, which interactively converts a suggestion into a commitable code comment, is available only for Qodo Merge Pro 💎 users.
## Example usage ## Example usage
### Manual triggering ### Manual triggering
Invoke the tool manually by commenting `/improve` on any PR. The code suggestions by default are presented as a single comment: Invoke the tool manually by commenting `/improve` on any PR. The code suggestions by default are presented as a single comment:
![code suggestions as comment](https://codium.ai/images/pr_agent/code_suggestions_as_comment.png){width=512}
To edit [configurations](#configuration-options) related to the improve tool, use the following template: To edit [configurations](#configuration-options) related to the improve tool, use the following template:
``` ```toml
/improve --pr_code_suggestions.some_config1=... --pr_code_suggestions.some_config2=... /improve --pr_code_suggestions.some_config1=... --pr_code_suggestions.some_config2=...
``` ```
For example, you can choose to present the suggestions as commitable code comments, by running the following command: For example, you can choose to present all the suggestions as commitable code comments, by running the following command:
``` ```toml
/improve --pr_code_suggestions.commitable_code_suggestions=true /improve --pr_code_suggestions.commitable_code_suggestions=true
``` ```
![improve](https://codium.ai/images/pr_agent/improve.png){width=512} ![improve](https://codium.ai/images/pr_agent/improve.png){width=512}
Note that a single comment has a significantly smaller PR footprint. We recommend this mode for most cases. As can be seen, a single table 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. Also note that collapsible are not supported in _Bitbucket_. Hence, the suggestions can only be presented in Bitbucket as code comments.
### Automatic triggering ### Automatic triggering
To run the `improve` automatically when a PR is opened, define in a [configuration file](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/#wiki-configuration-file): To run the `improve` automatically when a PR is opened, define in a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#wiki-configuration-file):
``` ```toml
[github_app] [github_app]
pr_commands = [ pr_commands = [
"/improve", "/improve",
@ -47,121 +52,250 @@ num_code_suggestions_per_chunk = ...
- The `pr_commands` lists commands that will be executed automatically when a PR is opened. - The `pr_commands` lists commands that will be executed automatically when a PR is opened.
- The `[pr_code_suggestions]` section contains the configurations for the `improve` tool you want to edit (if any) - The `[pr_code_suggestions]` section contains the configurations for the `improve` tool you want to edit (if any)
### Extended mode ### Assessing Impact 💎
An extended mode, which does not involve PR Compression and provides more comprehensive suggestions, can be invoked by commenting on any PR by setting: Note that Qodo Merge pro tracks two types of implementations:
```
[pr_code_suggestions]
auto_extended_mode=true
```
(This mode is 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). - Direct implementation - when the user directly applies the suggestion by clicking the `Apply` checkbox.
Hence, the total number of suggestions is proportional to the number of chunks, i.e., the size of the PR. - Indirect implementation - when the user implements the suggestion in their IDE environment. In this case, Qodo Merge will utilize, after each commit, a dedicated logic to identify if a suggestion was implemented, and will mark it as implemented.
![code_suggestions_asses_impact](https://codium.ai/images/pr_agent/code_suggestions_asses_impact.png){width=512}
In post-process, Qodo Merge counts the number of suggestions that were implemented, and provides general statistics and insights about the suggestions' impact on the PR process.
## Configuration options ![code_suggestions_asses_impact_stats_1](https://codium.ai/images/pr_agent/code_suggestions_asses_impact_stats_1.png){width=512}
!!! example "General options" ![code_suggestions_asses_impact_stats_2](https://codium.ai/images/pr_agent/code_suggestions_asses_impact_stats_2.png){width=512}
<table>
<tr>
<td><b>num_code_suggestions</b></td>
<td>Number of code suggestions provided by the 'improve' tool. Default is 4 for CLI, 0 for auto tools.</td>
</tr>
<tr>
<td><b>extra_instructions</b></td>
<td>Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".</td>
</tr>
<tr>
<td><b>rank_suggestions</b></td>
<td>If set to true, the tool will rank the suggestions, based on importance. Default is false.</td>
</tr>
<tr>
<td><b>commitable_code_suggestions</b></td>
<td>If set to true, the tool will display the suggestions as commitable code comments. Default is false.</td>
</tr>
<tr>
<td><b>persistent_comment</b></td>
<td>If set to true, the improve comment will be persistent, meaning that every new improve request will edit the previous one. Default is false.</td>
</tr>
<tr>
<td><b>self_reflect_on_suggestions</b></td>
<td>If set to true, the improve tool will calculate an importance score for each suggestion [1-10], and sort the suggestion labels group based on this score. Default is true.</td>
</tr>
<tr>
<td><b>suggestions_score_threshold</b></td>
<td> Any suggestion with importance score less than this threshold will be removed. Default is 0. Highly recommend not to set this value above 7-8, since above it may clip relevant suggestions that can be useful. </td>
</tr>
<tr>
<td><b>enable_help_text</b></td>
<td>If set to true, the tool will display a help text in the comment. Default is true.</td>
</tr>
</table>
!!! example "params for 'extended' mode"
<table>
<tr>
<td><b>auto_extended_mode</b></td>
<td>Enable extended mode automatically (no need for the --extended option). Default is true.</td>
</tr>
<tr>
<td><b>num_code_suggestions_per_chunk</b></td>
<td>Number of code suggestions provided by the 'improve' tool, per chunk. Default is 5.</td>
</tr>
<tr>
<td><b>rank_extended_suggestions</b></td>
<td>If set to true, the tool will rank the suggestions, based on importance. Default is true.</td>
</tr>
<tr>
<td><b>max_number_of_calls</b></td>
<td>Maximum number of chunks. Default is 5.</td>
</tr>
<tr>
<td><b>final_clip_factor</b></td>
<td>Factor to remove suggestions with low confidence. Default is 0.9.</td>
</tr>
</table>
## Usage Tips ## Usage Tips
!!! tip "Extra instructions" ### Implementing the proposed code suggestions
Each generated suggestion consists of three key elements:
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. 1. A single-line summary of the proposed change
2. An expandable section containing a comprehensive description of the suggestion
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. 3. A diff snippet showing the recommended code modification (before and after)
Examples for extra instructions: We advise users to apply critical analysis and judgment when implementing the proposed suggestions.
In addition to mistakes (which may happen, but are rare), sometimes the presented code modification may serve more as an _illustrative example_ than a direct applicable solution.
In such cases, we recommend prioritizing the suggestion's detailed description, using the diff snippet primarily as a supporting reference.
### Dual publishing mode
Our recommended approach for presenting code suggestions is through a [table](https://qodo-merge-docs.qodo.ai/tools/improve/#overview) (`--pr_code_suggestions.commitable_code_suggestions=false`).
This method significantly reduces the PR footprint and allows for quick and easy digestion of multiple suggestions.
We also offer a complementary **dual publishing mode**. When enabled, suggestions exceeding a certain score threshold are not only displayed in the table, but also presented as commitable PR comments.
This mode helps highlight suggestions deemed more critical.
To activate dual publishing mode, use the following setting:
```toml
[pr_code_suggestions]
dual_publishing_score_threshold = x
```
Where x represents the minimum score threshold (>=) for suggestions to be presented as commitable PR comments in addition to the table. Default is -1 (disabled).
### Self-review
If you set in a configuration file:
```toml
[pr_code_suggestions]
demand_code_suggestions_self_review = true
```
The `improve` tool will add a checkbox below the suggestions, prompting user to acknowledge that they have reviewed the suggestions.
You can set the content of the checkbox text via:
```toml
[pr_code_suggestions]
code_suggestions_self_review_text = "... (your text here) ..."
```
![self_review_1](https://codium.ai/images/pr_agent/self_review_1.png){width=512}
!!! tip "Tip - demanding self-review from the PR author 💎"
By setting:
```toml
[pr_code_suggestions]
approve_pr_on_self_review = true
``` ```
[pr_code_suggestions] # /improve # the tool can automatically add an approval when the PR author clicks the self-review checkbox.
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.
!!! tip "Review vs. Improve tools comparison"
- The [review](https://pr-agent-docs.codium.ai/tools/review/) tool includes a section called 'Possible issues', that also provide feedback on the PR Code. - If you set the number of required reviewers for a PR to 2, this effectively means that the PR author must click the self-review checkbox before the PR can be merged (in addition to a human reviewer).
In this section, the model is instructed to focus **only** on [major bugs and issues](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_reviewer_prompts.toml#L71).
- The `improve` tool, on the other hand, has a broader mandate, and in addition to bugs and issues, it can also give suggestions for improving code quality and making the code more efficient, readable, and maintainable (see [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/pr_code_suggestions_prompts.toml#L34)). ![self_review_2](https://codium.ai/images/pr_agent/self_review_2.png){width=512}
- Hence, if you are interested only in feedback about clear bugs, the `review` tool might suffice. If you want a more detailed feedback, including broader suggestions for improving the PR code, also enable the `improve` tool to run on each PR.
- If you keep the number of required reviewers for a PR to 1 and enable this configuration, this effectively means that the PR author can approve the PR by actively clicking the self-review checkbox.
To prevent unauthorized approvals, this configuration defaults to false, and cannot be altered through online comments; enabling requires a direct update to the configuration file and a commit to the repository. This ensures that utilizing the feature demands a deliberate documented decision by the repository owner.
### How many code suggestions are generated?
Qodo Merge uses a dynamic strategy to generate code suggestions based on the size of the pull request (PR). Here's how it works:
1) Chunking large PRs:
- Qodo Merge divides large PRs into 'chunks'.
- Each chunk contains up to `pr_code_suggestions.max_context_tokens` tokens (default: 14,000).
2) Generating suggestions:
- For each chunk, Qodo Merge generates up to `pr_code_suggestions.num_code_suggestions_per_chunk` suggestions (default: 4).
This approach has two main benefits:
- Scalability: The number of suggestions scales with the PR size, rather than being fixed.
- Quality: By processing smaller chunks, the AI can maintain higher quality suggestions, as larger contexts tend to decrease AI performance.
Note: Chunking is primarily relevant for large PRs. For most PRs (up to 500 lines of code), Qodo Merge will be able to process the entire code in a single call.
### 'Extra instructions' and 'best practices'
#### Extra instructions
>`Platforms supported: GitHub, GitLab, Bitbucket`
You can use the `extra_instructions` configuration option to give the AI model additional instructions for the `improve` tool.
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 possible instructions:
```toml
[pr_code_suggestions]
extra_instructions="""\
(1) Answer in japanese
(2) Don't suggest to add try-excpet block
(3) Ignore changes in toml files
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points or numbers to make the instructions more readable.
#### Best practices 💎
>`Platforms supported: GitHub, GitLab`
Another option to give additional guidance to the AI model is by creating a dedicated [**wiki page**](https://github.com/Codium-ai/pr-agent/wiki) called `best_practices.md`.
This page can contain a list of best practices, coding standards, and guidelines that are specific to your repo/organization.
The AI model will use this wiki page as a reference, and in case the PR code violates any of the guidelines, it will suggest improvements accordingly, with a dedicated label: `Organization
best practice`.
Example for a `best_practices.md` content can be found [here](https://github.com/Codium-ai/pr-agent/blob/main/docs/docs/usage-guide/EXAMPLE_BEST_PRACTICE.md) (adapted from Google's [pyguide](https://google.github.io/styleguide/pyguide.html)).
This file is only an example. Since it is used as a prompt for an AI model, we want to emphasize the following:
- It should be written in a clear and concise manner
- If needed, it should give short relevant code snippets as examples
- Recommended to limit the text to 800 lines or fewer. Heres why:
1) Extremely long best practices documents may not be fully processed by the AI model.
2) A lengthy file probably represent a more "**generic**" set of guidelines, which the AI model is already familiar with. The objective is to focus on a more targeted set of guidelines tailored to the specific needs of this project.
##### Local and global best practices
By default, Qodo Merge will look for a local `best_practices.md` wiki file in the root of the relevant local repo.
If you want to enable also a global `best_practices.md` wiki file, set first in the global configuration file:
```toml
[best_practices]
enable_global_best_practices = true
```
Then, create a `best_practices.md` wiki file in the root of [global](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#global-configuration-file) configuration repository, `pr-agent-settings`.
##### Example results
![best_practice](https://codium.ai/images/pr_agent/org_best_practice.png){width=512}
#### How to combine `extra instructions` and `best practices`
The `extra instructions` configuration is more related to the `improve` tool prompt. It can be used, for example, to avoid specific suggestions ("Don't suggest to add try-except block", "Ignore changes in toml files", ...) or to emphasize specific aspects or formats ("Answer in Japanese", "Give only short suggestions", ...)
In contrast, the `best_practices.md` file is a general guideline for the way code should be written in the repo.
Using a combination of both can help the AI model to provide relevant and tailored suggestions.
## Configuration options
??? example "General options"
<table>
<tr>
<td><b>extra_instructions</b></td>
<td>Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".</td>
</tr>
<tr>
<td><b>commitable_code_suggestions</b></td>
<td>If set to true, the tool will display the suggestions as commitable code comments. Default is false.</td>
</tr>
<tr>
<td><b>dual_publishing_score_threshold</b></td>
<td>Minimum score threshold for suggestions to be presented as commitable PR comments in addition to the table. Default is -1 (disabled).</td>
</tr>
<tr>
<td><b>persistent_comment</b></td>
<td>If set to true, the improve comment will be persistent, meaning that every new improve request will edit the previous one. Default is false.</td>
</tr>
<tr>
<td><b>self_reflect_on_suggestions</b></td>
<td>If set to true, the improve tool will calculate an importance score for each suggestion [1-10], and sort the suggestion labels group based on this score. Default is true.</td>
</tr>
<tr>
<td><b>suggestions_score_threshold</b></td>
<td> Any suggestion with importance score less than this threshold will be removed. Default is 0. Highly recommend not to set this value above 7-8, since above it may clip relevant suggestions that can be useful. </td>
</tr>
<tr>
<td><b>apply_suggestions_checkbox</b></td>
<td> Enable the checkbox to create a committable suggestion. Default is true.</td>
</tr>
<tr>
<td><b>enable_help_text</b></td>
<td>If set to true, the tool will display a help text in the comment. Default is true.</td>
</tr>
<tr>
<td><b>enable_chat_text</b></td>
<td>If set to true, the tool will display a reference to the PR chat in the comment. Default is true.</td>
</tr>
</table>
??? example "Params for number of suggestions and AI calls"
<table>
<tr>
<td><b>auto_extended_mode</b></td>
<td>Enable chunking the PR code and running the tool on each chunk. Default is true.</td>
</tr>
<tr>
<td><b>num_code_suggestions_per_chunk</b></td>
<td>Number of code suggestions provided by the 'improve' tool, per chunk. Default is 4.</td>
</tr>
<tr>
<td><b>max_number_of_calls</b></td>
<td>Maximum number of chunks. Default is 3.</td>
</tr>
<tr>
<td><b>rank_extended_suggestions</b></td>
<td>If set to true, the tool will rank the suggestions, based on importance. Default is true.</td>
</tr>
</table>
## A note on code suggestions quality ## 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. Critical reading and judgment are required. - AI models for code are getting better and better (Sonnet-3.5 and GPT-4), but they are not flawless. Not all the suggestions will be perfect, and a user should not accept all of them automatically. Critical reading and judgment are required.
- While mistakes of the AI are rare but can happen, a real benefit from the suggestions of the `improve` (and [`review`](https://pr-agent-docs.codium.ai/tools/review/)) tool is to catch, with high probability, **mistakes or bugs done by the PR author**, when they happen. So, it's a good practice to spend the needed ~30-60 seconds to review the suggestions, even if not all of them are always relevant. - While mistakes of the AI are rare but can happen, a real benefit from the suggestions of the `improve` (and [`review`](https://qodo-merge-docs.qodo.ai/tools/review/)) tool is to catch, with high probability, **mistakes or bugs done by the PR author**, when they happen. So, it's a good practice to spend the needed ~30-60 seconds to review the suggestions, even if not all of them are always relevant.
- The hierarchical structure of the suggestions is designed to help the user to _quickly_ understand them, and to decide which ones are relevant and which are not: - The hierarchical structure of the suggestions is designed to help the user to _quickly_ understand them, and to decide which ones are relevant and which are not:
- Only if the `Category` header is relevant, the user should move to the summarized suggestion description - Only if the `Category` header is relevant, the user should move to the summarized suggestion description
- Only if the summarized suggestion description is relevant, the user should click on the collapsible, to read the full suggestion description with a code preview example. - Only if the summarized suggestion description is relevant, the user should click on the collapsible, to read the full suggestion description with a code preview example.
In addition, we recommend to use the `exra_instructions` field to guide the model to suggestions that are more relevant to the specific needs of the project. - In addition, we recommend to use the [`extra_instructions`](https://qodo-merge-docs.qodo.ai/tools/improve/#extra-instructions-and-best-practices) field to guide the model to suggestions that are more relevant to the specific needs of the project.
<br> - The interactive [PR chat](https://qodo-merge-docs.qodo.ai/chrome-extension/) also provides an easy way to get more tailored suggestions and feedback from the AI model.
Consider also trying the [Custom Prompt Tool](./custom_prompt.md) 💎, that will **only** propose code suggestions that follow specific guidelines defined by user.

View File

@ -1,6 +1,6 @@
# Tools # Tools
Here is a list of PR-Agent tools, each with a dedicated page that explains how to use it: Here is a list of Qodo Merge tools, each with a dedicated page that explains how to use it:
| Tool | Description | | Tool | Description |
|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| |------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
@ -19,4 +19,4 @@ Here is a list of PR-Agent tools, each with a dedicated page that explains how t
| **💎 [Improve Component (`/improve_component component_name`](./improve_component.md))** | Generates code suggestions for a specific code component that changed in the PR | | **💎 [Improve Component (`/improve_component component_name`](./improve_component.md))** | Generates code suggestions for a specific code component that changed in the PR |
| **💎 [CI Feedback (`/checks ci_job`](./ci_feedback.md))** | Automatically generates feedback and analysis for a failed CI job | | **💎 [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. Note that the tools marked with 💎 are available only for Qodo Merge Pro users.

View File

@ -1,10 +1,16 @@
## Overview ## Overview
The `review` tool scans the PR code changes, and automatically generates a PR review. The `review` tool scans the PR code changes, and generates a list of feedbacks about the PR, aiming to aid the reviewing process.
<br>
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: 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 /review
``` ```
Note that the main purpose of the `review` tool is to provide the **PR reviewer** with useful feedbacks and insights. The PR author, in contrast, may prefer to save time and focus on the output of the [improve](./improve.md) tool, which provides actionable code suggestions.
(Read more about the different personas in the PR process and how Qodo Merge aims to assist them in our [blog](https://www.codium.ai/blog/understanding-the-challenges-and-pain-points-of-the-pull-request-cycle/))
## Example usage ## Example usage
### Manual triggering ### Manual triggering
@ -24,7 +30,7 @@ If you want to edit [configurations](#configuration-options), add the relevant o
### Automatic triggering ### Automatic triggering
To run the `review` automatically when a PR is opened, define in a [configuration file](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/#wiki-configuration-file): To run the `review` automatically when a PR is opened, define in a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#wiki-configuration-file):
``` ```
[github_app] [github_app]
pr_commands = [ pr_commands = [
@ -40,29 +46,45 @@ num_code_suggestions = ...
- The `pr_commands` lists commands that will be executed automatically when a PR is opened. - The `pr_commands` lists commands that will be executed automatically when a PR is opened.
- The `[pr_reviewer]` section contains the configurations for the `review` tool you want to edit (if any). - The `[pr_reviewer]` section contains the configurations for the `review` tool you want to edit (if any).
### 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. [//]: # (### Incremental Mode)
For invoking the incremental mode, the following command can be used:
```
/review -i
```
Note that the incremental mode is only available for GitHub.
![incremental review](https://codium.ai/images/pr_agent/incremental_review_2.png){width=512} [//]: # (Incremental review only considers changes since the last Qodo Merge 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.)
### PR Reflection [//]: # (For invoking the incremental mode, the following command can be used:)
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.
![reflection questions](https://codium.ai/images/pr_agent/reflection_questions.png){width=512} [//]: # (/review -i)
![reflection answers](https://codium.ai/images/pr_agent/reflection_answers.png){width=512} [//]: # (```)
![reflection insights](https://codium.ai/images/pr_agent/reflection_insights.png){width=512} [//]: # (Note that the incremental mode is only available for GitHub.)
[//]: # ()
[//]: # (![incremental review]&#40;https://codium.ai/images/pr_agent/incremental_review_2.png&#41;{width=512})
[//]: # (### 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.)
[//]: # ()
[//]: # (![reflection questions]&#40;https://codium.ai/images/pr_agent/reflection_questions.png&#41;{width=512})
[//]: # ()
[//]: # (![reflection answers]&#40;https://codium.ai/images/pr_agent/reflection_answers.png&#41;{width=512})
[//]: # ()
[//]: # (![reflection insights]&#40;https://codium.ai/images/pr_agent/reflection_insights.png&#41;{width=512})
@ -73,11 +95,11 @@ The tool will first ask the author questions about the PR, and will guide the re
<table> <table>
<tr> <tr>
<td><b>num_code_suggestions</b></td> <td><b>num_code_suggestions</b></td>
<td>Number of code suggestions provided by the 'review' tool. For manual comments, default is 4. For PR-Agent app auto tools, default is 0, meaning no code suggestions will be provided by the review tool, unless you manually edit pr_commands.</td> <td>Number of code suggestions provided by the 'review' tool. Default is 0, meaning no code suggestions will be provided by the `review` tool.</td>
</tr> </tr>
<tr> <tr>
<td><b>inline_code_comments</b></td> <td><b>inline_code_comments</b></td>
<td>If set to true, the tool will publish the code suggestions as comments on the code diff. Default is false.</td> <td>If set to true, the tool will publish the code suggestions as comments on the code diff. Default is false. Note that you need to set `num_code_suggestions`>0 to get code suggestions </td>
</tr> </tr>
<tr> <tr>
<td><b>persistent_comment</b></td> <td><b>persistent_comment</b></td>
@ -112,6 +134,10 @@ The tool will first ask the author questions about the PR, and will guide the re
<td><b>require_can_be_split_review</b></td> <td><b>require_can_be_split_review</b></td>
<td>If set to true, the tool will add a section that checks if the PR contains several themes, and can be split into smaller PRs. Default is false.</td> <td>If set to true, the tool will add a section that checks if the PR contains several themes, and can be split into smaller PRs. Default is false.</td>
</tr> </tr>
<tr>
<td><b>require_security_review</b></td>
<td>If set to true, the tool will add a section that checks if the PR contains a possible security or vulnerability issue. Default is true.</td>
</tr>
</table> </table>
!!! example "SOC2 ticket compliance 💎" !!! example "SOC2 ticket compliance 💎"
@ -151,7 +177,7 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
<table> <table>
<tr> <tr>
<td><b>enable_auto_approval</b></td> <td><b>enable_auto_approval</b></td>
<td>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.</td> <td>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 a configuration file.</td>
</tr> </tr>
<tr> <tr>
<td><b>maximal_review_effort</b></td> <td><b>maximal_review_effort</b></td>
@ -163,7 +189,7 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
!!! tip "General guidelines" !!! tip "General guidelines"
The `review` tool provides a collection of possible feedbacks about a PR. The `review` tool provides a collection of configurable feedbacks about a PR.
It is recommended to review the [Configuration options](#configuration-options) section, and choose the relevant options for your use case. 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: Some of the features that are disabled by default are quite useful, and should be considered for enabling. For example:
@ -172,19 +198,12 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
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. 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.
!!! tip "Automation" !!! tip "Automation"
When you first install PR-Agent 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: When you first install Qodo Merge 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 --pr_reviewer.num_code_suggestions=0", ...] pr_commands = ["/review --pr_reviewer.num_code_suggestions=0", ...]
``` ```
Meaning the `review` tool will run automatically on every PR, without providing code suggestions. Meaning the `review` tool will run automatically on every PR, without providing code suggestions.
Edit this field to enable/disable the tool, or to change the used configurations. Edit this field to enable/disable the tool, or to change the configurations used.
!!! tip "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.
!!! tip "Possible labels from the review tool" !!! tip "Possible labels from the review tool"
@ -202,7 +221,7 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
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. 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: Examples of extra instructions:
``` ```
[pr_reviewer] [pr_reviewer]
extra_instructions="""\ extra_instructions="""\
@ -218,7 +237,7 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
!!! tip "Auto-approval" !!! tip "Auto-approval"
PR-Agent can approve a PR when a specific comment is invoked. Qodo Merge 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: 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:
``` ```
@ -232,7 +251,7 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
``` ```
/review auto_approve /review auto_approve
``` ```
PR-Agent will automatically approve the PR, and add a comment with the approval. Qodo Merge 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: 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:
@ -240,3 +259,14 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
[pr_reviewer] [pr_reviewer]
maximal_review_effort = 5 maximal_review_effort = 5
``` ```
[//]: # (!!! tip "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`]&#40;./improve.md&#41; 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.)

View File

@ -8,9 +8,9 @@ For example:
![similar code global](https://codium.ai/images/pr_agent/similar_code_global2.png){width=768} ![similar code global](https://codium.ai/images/pr_agent/similar_code_global2.png){width=768}
PR-Agent will examine the code component and will extract the most relevant keywords to search for similar code: Qodo Merge 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. - `extracted keywords`: the keywords that were extracted from the code by Qodo Merge. 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). - `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. - `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. - `relevant repositories`: the open-source repositories in which that are relevant to the searched code component and it's keywords.

View File

@ -33,7 +33,7 @@ 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`

View File

@ -10,14 +10,10 @@ To get a list of the components that changed in the PR and choose the relevant c
## Example usage ## Example usage
Invoke the tool manually by commenting `/test` on any PR: Invoke the tool manually by commenting `/test` on any PR:
![test1](https://codium.ai/images/pr_agent/test1.png){width=704}
The tool will generate tests for the selected component (if no component is stated, it will generate tests for largest component): The tool will generate tests for the selected component (if no component is stated, it will generate tests for largest component):
![test2](https://codium.ai/images/pr_agent/test2.png){width=768} ![test1](https://codium.ai/images/pr_agent/test1.png){width=768}
![test3](https://codium.ai/images/pr_agent/test3.png){width=768}
(Example taken from [here](https://github.com/Codium-ai/pr-agent/pull/598#issuecomment-1913679429)): (Example taken from [here](https://github.com/Codium-ai/pr-agent/pull/598#issuecomment-1913679429)):

View File

@ -0,0 +1,189 @@
## Recommend Python Best Practices
This document outlines a series of recommended best practices for Python development. These guidelines aim to improve code quality, maintainability, and readability.
### Imports
Use `import` statements for packages and modules only, not for individual types, classes, or functions.
#### Definition
Reusability mechanism for sharing code from one module to another.
#### Decision
- Use `import x` for importing packages and modules.
- Use `from x import y` where `x` is the package prefix and `y` is the module name with no prefix.
- Use `from x import y as z` in any of the following circumstances:
- Two modules named `y` are to be imported.
- `y` conflicts with a top-level name defined in the current module.
- `y` conflicts with a common parameter name that is part of the public API (e.g., `features`).
- `y` is an inconveniently long name, or too generic in the context of your code
- Use `import y as z` only when `z` is a standard abbreviation (e.g., `import numpy as np`).
For example the module `sound.effects.echo` may be imported as follows:
```
from sound.effects import echo
...
echo.EchoFilter(input, output, delay=0.7, atten=4)
```
Do not use relative names in imports. Even if the module is in the same package, use the full package name. This helps prevent unintentionally importing a package twice.
##### Exemptions
Exemptions from this rule:
- Symbols from the following modules are used to support static analysis and type checking:
- [`typing` module](https://google.github.io/styleguide/pyguide.html#typing-imports)
- [`collections.abc` module](https://google.github.io/styleguide/pyguide.html#typing-imports)
- [`typing_extensions` module](https://github.com/python/typing_extensions/blob/main/README.md)
- Redirects from the [six.moves module](https://six.readthedocs.io/#module-six.moves).
### Packages
Import each module using the full pathname location of the module.
#### Decision
All new code should import each module by its full package name.
Imports should be as follows:
```
Yes:
# Reference absl.flags in code with the complete name (verbose).
import absl.flags
from doctor.who import jodie
_FOO = absl.flags.DEFINE_string(...)
```
```
Yes:
# Reference flags in code with just the module name (common).
from absl import flags
from doctor.who import jodie
_FOO = flags.DEFINE_string(...)
```
_(assume this file lives in `doctor/who/` where `jodie.py` also exists)_
```
No:
# Unclear what module the author wanted and what will be imported. The actual
# import behavior depends on external factors controlling sys.path.
# Which possible jodie module did the author intend to import?
import jodie
```
The directory the main binary is located in should not be assumed to be in `sys.path` despite that happening in some environments. This being the case, code should assume that `import jodie` refers to a third-party or top-level package named `jodie`, not a local `jodie.py`.
### Default Iterators and Operators
Use default iterators and operators for types that support them, like lists, dictionaries, and files.
#### Definition
Container types, like dictionaries and lists, define default iterators and membership test operators (“in” and “not in”).
#### Decision
Use default iterators and operators for types that support them, like lists, dictionaries, and files. The built-in types define iterator methods, too. Prefer these methods to methods that return lists, except that you should not mutate a container while iterating over it.
```
Yes: for key in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in adict.items(): ...
```
```
No: for key in adict.keys(): ...
for line in afile.readlines(): ...
```
### Lambda Functions
Okay for one-liners. Prefer generator expressions over `map()` or `filter()` with a `lambda`.
#### Decision
Lambdas are allowed. If the code inside the lambda function spans multiple lines or is longer than 60-80 chars, it might be better to define it as a regular [nested function](https://google.github.io/styleguide/pyguide.html#lexical-scoping).
For common operations like multiplication, use the functions from the `operator` module instead of lambda functions. For example, prefer `operator.mul` to `lambda x, y: x * y`.
### Default Argument Values
Okay in most cases.
#### Definition
You can specify values for variables at the end of a functions parameter list, e.g., `def foo(a, b=0):`. If `foo` is called with only one argument, `b` is set to 0. If it is called with two arguments, `b` has the value of the second argument.
#### Decision
Okay to use with the following caveat:
Do not use mutable objects as default values in the function or method definition.
```
Yes: def foo(a, b=None):
if b is None:
b = []
Yes: def foo(a, b: Sequence | None = None):
if b is None:
b = []
Yes: def foo(a, b: Sequence = ()): # Empty tuple OK since tuples are immutable.
...
```
```
from absl import flags
_FOO = flags.DEFINE_string(...)
No: def foo(a, b=[]):
...
No: def foo(a, b=time.time()): # Is `b` supposed to represent when this module was loaded?
...
No: def foo(a, b=_FOO.value): # sys.argv has not yet been parsed...
...
No: def foo(a, b: Mapping = {}): # Could still get passed to unchecked code.
...
```
### True/False Evaluations
Use the “implicit” false if possible, e.g., `if foo:` rather than `if foo != []:`
### Lexical Scoping
Okay to use.
An example of the use of this feature is:
```
def get_adder(summand1: float) -> Callable[[float], float]:
"""Returns a function that adds numbers to a given number."""
def adder(summand2: float) -> float:
return summand1 + summand2
return adder
```
#### Decision
Okay to use.
### Threading
Do not rely on the atomicity of built-in types.
While Pythons built-in data types such as dictionaries appear to have atomic operations, there are corner cases where they arent atomic (e.g. if `__hash__` or `__eq__` are implemented as Python methods) and their atomicity should not be relied upon. Neither should you rely on atomic variable assignment (since this in turn depends on dictionaries).
Use the `queue` modules `Queue` data type as the preferred way to communicate data between threads. Otherwise, use the `threading` module and its locking primitives. Prefer condition variables and `threading.Condition` instead of using lower-level locks.

View File

@ -0,0 +1,18 @@
## Qodo Merge Pro Models
The default models used by Qodo Merge Pro are a combination of Claude-3.5-sonnet and OpenAI's GPT-4 models.
Users can configure Qodo Merge Pro to use solely a specific model by editing the [configuration](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/) file.
For example, to restrict Qodo Merge Pro to using only `Claude-3.5-sonnet`, add this setting:
```
[config]
model="claude-3-5-sonnet"
```
Or to restrict Qodo Merge Pro to using only `GPT-4o`, add this setting:
```
[config]
model="gpt-4o"
```

View File

@ -1,24 +1,58 @@
## Show possible configurations
The possible configurations of Qodo Merge are stored in [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml).
In the [tools](https://qodo-merge-docs.qodo.ai/tools/) page you can find explanations on how to use these configurations for each tool.
To print all the available configurations as a comment on your PR, you can use the following command:
```
/config
```
![possible_config1](https://codium.ai/images/pr_agent/possible_config1.png){width=512}
To view the **actual** configurations used for a specific tool, after all the user settings are applied, you can add for each tool a `--config.output_relevant_configurations=true` suffix.
For example:
```
/improve --config.output_relevant_configurations=true
```
Will output an additional field showing the actual configurations used for the `improve` tool.
![possible_config2](https://codium.ai/images/pr_agent/possible_config2.png){width=512}
## Ignoring files from analysis ## Ignoring files from analysis
In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code. In some cases, you may want to exclude specific files or directories from the analysis performed by Qodo Merge. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendor 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:
You can ignore files or folders using the following methods:
- `IGNORE.GLOB` - `IGNORE.GLOB`
- `IGNORE.REGEX` - `IGNORE.REGEX`
For example, to ignore Python files in a PR with online usage, comment on a PR: which you can edit to ignore files or folders based on glob or regex patterns.
`/review --ignore.glob=['*.py']`
To ignore Python files in all PRs, set in a configuration file: ### Example usage
Let's look at an example where we want to ignore all files with `.py` extension from the analysis.
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 using `glob` pattern, set in a configuration file:
``` ```
[ignore] [ignore]
glob = ['*.py'] glob = ['*.py']
``` ```
And to ignore Python files in all PRs using `regex` pattern, set in a configuration file:
```
[regex]
regex = ['.*\.py$']
```
## Extra instructions ## Extra instructions
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage: All Qodo Merge 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 ..." /update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
``` ```
@ -30,173 +64,11 @@ This mode provides a very good speed-quality-cost tradeoff, and can handle most
When the PR is above the token limit, it employs a [PR Compression strategy](../core-abilities/index.md). 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 two possible solutions: However, for very large PRs, or in case you want to emphasize quality over speed and cost, there are two 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. 1) [Use a model](https://qodo-merge-docs.qodo.ai/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`), 2) For the `/improve` tool, there is an ['extended' mode](https://qodo-merge-docs.qodo.ai/tools/improve/) (`/improve --extended`),
which divides the PR to chunks, and processes each chunk separately. With this mode, regardless of the model, no compression will be done (but for large PRs, multiple model calls may occur) which divides the PR into chunks, and processes 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"
model_turbo = "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"
model_turbo = "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"
model_turbo = "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 instructions on how to set keys for other models.
### Groq
To use Llama3 model with Groq, for example, set:
```
[config] # in configuration.toml
model = "llama3-70b-8192"
model_turbo = "llama3-70b-8192"
fallback_models = ["groq/llama3-70b-8192"]
[groq] # in .secrets.toml
key = ... # your Groq api key
```
(you can obtain a Groq key from [here](https://console.groq.com/keys))
### 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"
model_turbo = "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="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
model_turbo="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
fallback_models=["bedrock/anthropic.claude-v2:1"]
[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.
If you are using the claude-3 model, please configure the following settings as there are parameters incompatible with claude-3.
```
[litellm]
drop_params = true
```
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 ## Patch Extra Lines
@ -213,19 +85,21 @@ By default, around any change in your PR, git patch provides three lines of cont
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: Qodo Merge will try to increase the number of lines of context, via the parameter:
``` ```
[config] [config]
patch_extra_lines=3 patch_extra_lines_before=3
patch_extra_lines_after=1
``` ```
Increasing this number provides more context to the model, but will also increase the token budget. Increasing this number provides more context to the model, but will also increase the token budget, and may overwhelm the model with too much information, unrelated to the actual PR code changes.
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.
If the PR is too large (see [PR Compression strategy](https://github.com/Codium-ai/pr-agent/blob/main/PR_COMPRESSION.md)), Qodo Merge may automatically set this number to 0, and will use the original git patch.
## Editing the prompts ## Editing the prompts
The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder. The prompts for the various Qodo Merge tools are defined in the `pr_agent/settings` folder.
In practice, the prompts are loaded and stored as a standard setting object. 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. 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.
@ -240,3 +114,49 @@ 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). 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).
## Integrating with Logging Observability Platforms
Various logging observability tools can be used out-of-the box when using the default LiteLLM AI Handler. Simply configure the LiteLLM callback settings in `configuration.toml` and set environment variables according to the LiteLLM [documentation](https://docs.litellm.ai/docs/).
For example, to use [LangSmith](https://www.langchain.com/langsmith) you can add the following to your `configuration.toml` file:
```
[litellm]
enable_callbacks = true
success_callback = ["langsmith"]
failure_callback = ["langsmith"]
service_callback = []
```
Then set the following environment variables:
```
LANGSMITH_API_KEY=<api_key>
LANGSMITH_PROJECT=<project>
LANGSMITH_BASE_URL=<url>
```
## Ignoring automatic commands in PRs
In some cases, you may want to automatically ignore specific PRs . Qodo Merge enables you to ignore PR with a specific title, or from/to specific branches (regex matching).
To ignore PRs with a specific title such as "[Bump]: ...", you can add the following to your `configuration.toml` file:
```
[config]
ignore_pr_title = ["\\[Bump\\]"]
```
Where the `ignore_pr_title` is a list of regex patterns to match the PR title you want to ignore. Default is `ignore_pr_title = ["^\\[Auto\\]", "^Auto"]`.
To ignore PRs from specific source or target branches, you can add the following to your `configuration.toml` file:
```
[config]
ignore_pr_source_branches = ['develop', 'main', 'master', 'stage']
ignore_pr_target_branches = ["qa"]
```
Where the `ignore_pr_source_branches` and `ignore_pr_target_branches` are lists of regex patterns to match the source and target branches you want to ignore.
They are not mutually exclusive, you can use them together or separately.

View File

@ -1,5 +1,5 @@
## Local repo (CLI) ## Local repo (CLI)
When running from your locally cloned PR-Agent repo (CLI), your local configuration file will be used. When running from your locally cloned Qodo Merge repo (CLI), your local configuration file will be used.
Examples of invoking the different tools via the CLI: Examples of invoking the different tools via the CLI:
- **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review` - **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review`
@ -26,15 +26,25 @@ verbosity_level=2
``` ```
This is useful for debugging or experimenting with different tools. This is useful for debugging or experimenting with different tools.
(3)
**git provider**: The [git_provider](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L5) field in a configuration file determines the GIT provider that will be used by Qodo Merge. Currently, the following providers are supported:
`
"github", "gitlab", "bitbucket", "azure", "codecommit", "local", "gerrit"
`
Default is "github".
### Online usage ### 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. Online usage means invoking Qodo Merge 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: Commands for invoking the different tools via comments:
- **Review**: `/review` - **Review**: `/review`
- **Describe**: `/describe` - **Describe**: `/describe`
- **Improve**: `/improve` - **Improve**: `/improve` (or `/improve_code` for bitbucket, since `/improve` is sometimes reserved)
- **Ask**: `/ask "..."` - **Ask**: `/ask "..."`
- **Reflect**: `/reflect` - **Reflect**: `/reflect`
- **Update Changelog**: `/update_changelog` - **Update Changelog**: `/update_changelog`
@ -50,8 +60,8 @@ Any configuration value in [configuration file](https://github.com/Codium-ai/pr-
## GitHub App ## GitHub App
!!! note "Configurations for PR-Agent Pro" !!! note "Configurations for Qodo Merge Pro"
PR-Agent Pro for GitHub is an App, hosted by CodiumAI. So all the instructions below are relevant also for PR-Agent Pro users. Qodo Merge Pro for GitHub is an App, hosted by CodiumAI. So all the instructions below are relevant also for Qodo Merge Pro users.
Same goes for [GitLab webhook](#gitlab-webhook) and [BitBucket App](#bitbucket-app) sections. Same goes for [GitLab webhook](#gitlab-webhook) and [BitBucket App](#bitbucket-app) sections.
### GitHub app automatic tools when a new PR is opened ### GitHub app automatic tools when a new PR is opened
@ -67,10 +77,10 @@ pr_commands = [
"/improve", "/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. This means that when a new PR is opened/reopened or marked as ready for review, Qodo Merge will run the `describe`, `review` and `improve` tools.
For the `review` tool, for example, the `num_code_suggestions` parameter will be set to 0. For the `review` tool, for example, the `num_code_suggestions` parameter will be set to 0.
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**. You can override the default tool parameters by using one the three options for a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/): **wiki**, **local**, or **global**.
For example, if your local `.pr_agent.toml` file contains: For example, if your local `.pr_agent.toml` file contains:
``` ```
[pr_description] [pr_description]
@ -81,16 +91,9 @@ Every time you run the `describe` tool, including automatic runs, the PR title w
To cancel the automatic run of all the tools, set: To cancel the automatic run of all the tools, set:
``` ```
[github_app] [github_app]
handle_pr_actions = [] pr_commands = []
``` ```
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) ### 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. 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.
@ -105,10 +108,10 @@ push_commands = [
"/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false", "/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
] ]
``` ```
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. This means that when new code is pushed to the PR, the Qodo Merge will run the `describe` and `review` tools, with the specified parameters.
## GitHub Action ## GitHub Action
`GitHub Action` is a different way to trigger PR-Agent tools, and uses a different configuration mechanism than `GitHub App`.<br> `GitHub Action` is a different way to trigger Qodo Merge tools, and uses a different configuration mechanism than `GitHub App`.<br>
You can configure settings for `GitHub Action` by adding environment variables under the env section in `.github/workflows/pr_agent.yml` file. 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: Specifically, start by setting the following environment variables:
```yaml ```yaml
@ -118,26 +121,29 @@ Specifically, start by setting the following environment variables:
github_action_config.auto_review: "true" # enable\disable auto review github_action_config.auto_review: "true" # enable\disable auto review
github_action_config.auto_describe: "true" # enable\disable auto describe github_action_config.auto_describe: "true" # enable\disable auto describe
github_action_config.auto_improve: "true" # enable\disable auto improve github_action_config.auto_improve: "true" # enable\disable auto improve
github_action_config.enable_output: "true" # enable\disable github actions output parameter github_action_config.pr_actions: ["opened", "reopened", "ready_for_review", "review_requested"]
``` ```
`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. `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. If not set, the default configuration is for all three tools to run automatically when a new PR is opened.
`github_action_config.pr_actions` is used to configure which `pull_requests` events will trigger the enabled auto flags
If not set, the default configuration is `["opened", "reopened", "ready_for_review", "review_requested"]`
`github_action_config.enable_output` are used to enable/disable github actions [output parameter](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions) (default is `true`). `github_action_config.enable_output` are used to enable/disable github actions [output parameter](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions) (default is `true`).
Review result is output as JSON to `steps.{step-id}.outputs.review` property. Review result is output as JSON to `steps.{step-id}.outputs.review` property.
The JSON structure is equivalent to the yaml data structure defined in [pr_reviewer_prompts.toml](https://github.com/idubnori/pr-agent/blob/main/pr_agent/settings/pr_reviewer_prompts.toml). The JSON structure is equivalent to the yaml data structure defined in [pr_reviewer_prompts.toml](https://github.com/idubnori/pr-agent/blob/main/pr_agent/settings/pr_reviewer_prompts.toml).
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` [configuration file](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/#global-configuration-file) in the root of your repo 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` [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#global-configuration-file) in the root of your repo
For example, you can set an environment variable: `pr_description.publish_labels=false`, or add a `.pr_agent.toml` file with the following content: For example, you can set an environment variable: `pr_description.publish_labels=false`, or add a `.pr_agent.toml` file with the following content:
``` ```
[pr_description] [pr_description]
publish_labels = false publish_labels = false
``` ```
to prevent PR-Agent from publishing labels when running the `describe` tool. to prevent Qodo Merge from publishing labels when running the `describe` tool.
## GitLab Webhook ## 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: After setting up a GitLab webhook, to control which commands will run automatically when a new MR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
``` ```
[gitlab] [gitlab]
pr_commands = [ pr_commands = [
@ -147,18 +153,38 @@ pr_commands = [
] ]
``` ```
the GitLab webhook can also respond to new code that is pushed to an open MR.
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 MR.
```
[gitlab]
handle_push_trigger = true
push_commands = [
"/describe",
"/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
]
```
Note that to use the 'handle_push_trigger' feature, you need to give the gitlab webhook also the "Push events" scope.
## BitBucket App ## 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. Similar to GitHub app, when running Qodo Merge 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. 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: For example, if your local `.pr_agent.toml` file contains:
``` ```
[pr_reviewer] [pr_reviewer]
inline_code_comments = true extra_instructions = "Answer in japanese"
``` ```
Each time you invoke a `/review` tool, it will use inline code comments. Each time you invoke a `/review` tool, it will use the extra instructions you set in the local configuration file.
Note that among other limitations, BitBucket provides relatively low rate-limits for applications (up to 1000 requests per hour), and does not provide an API to track the actual rate-limit usage.
If you experience lack of responses from Qodo Merge, you might want to set: `bitbucket_app.avoid_full_files=true` in your configuration file.
This will prevent Qodo Merge from acquiring the full file content, and will only use the diff content. This will reduce the number of requests made to BitBucket, at the cost of small decrease in accuracy, as dynamic context will not be applicable.
### BitBucket Self-Hosted App automatic tools ### BitBucket Self-Hosted App automatic tools
@ -169,9 +195,11 @@ Specifically, set the following values:
[bitbucket_app] [bitbucket_app]
pr_commands = [ pr_commands = [
"/review --pr_reviewer.num_code_suggestions=0", "/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.commitable_code_suggestions=true", "/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
] ]
``` ```
Note that we set specifically for bitbucket, we recommend using: `--pr_code_suggestions.suggestions_score_threshold=7` and that is the default value we set for bitbucket.
Since this platform only supports inline code suggestions, we want to limit the number of suggestions, and only present a limited number.
## Azure DevOps provider ## Azure DevOps provider

View File

@ -0,0 +1,189 @@
## Changing a model
See [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/algo/__init__.py) for a list of available models.
To use a different model than the default (GPT-4), you need to edit in the [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L2) the fields:
```
[config]
model = "..."
model_turbo = "..."
fallback_models = ["..."]
```
For models and environments not from OpenAI, you might need to provide additional keys and other parameters.
You can give parameters via a configuration file (see below for instructions), or from environment variables. See [litellm documentation](https://litellm.vercel.app/docs/proxy/quick_start#supported-llms) for the environment variables relevant per model.
### 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)
model_turbo="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
fallback_models=["..."] # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
```
### Hugging Face
**Local**
You can run Hugging Face 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 Hugging Face 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"
model_turbo = "ollama/llama2"
fallback_models=["ollama/llama2"]
[ollama] # in .secrets.toml
api_base = ... # the base url for your Hugging Face inference endpoint
# e.g. if running Ollama locally, you may use:
api_base = "http://localhost:11434/"
```
### Inference Endpoints
To use a new model with Hugging Face 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"
model_turbo = "huggingface/meta-llama/Llama-2-7b-chat-hf"
fallback_models=["huggingface/meta-llama/Llama-2-7b-chat-hf"]
[huggingface] # in .secrets.toml
key = ... # your Hugging Face api key
api_base = ... # the base url for your Hugging Face 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"
model_turbo = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
fallback_models=["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 instructions on how to set keys for other models.
### Groq
To use Llama3 model with Groq, for example, set:
```
[config] # in configuration.toml
model = "llama3-70b-8192"
model_turbo = "llama3-70b-8192"
fallback_models = ["groq/llama3-70b-8192"]
[groq] # in .secrets.toml
key = ... # your Groq api key
```
(you can obtain a Groq key from [here](https://console.groq.com/keys))
### 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"
model_turbo = "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="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
model_turbo="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
fallback_models=["bedrock/anthropic.claude-v2: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.
If you are using the claude-3 model, please configure the following settings as there are parameters incompatible with claude-3.
```
[litellm]
drop_params = true
```
AWS session is automatically authenticated from your environment, but you can also explicitly set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION_NAME` environment variables. Please refer to [this document](https://litellm.vercel.app/docs/providers/bedrock) for more details.
### Custom models
If the relevant model doesn't appear [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/algo/__init__.py), you can still use it as a custom model:
(1) Set the model name in the configuration file:
```
[config]
model="custom_model_name"
model_turbo="custom_model_name"
fallback_models=["custom_model_name"]
```
(2) Set the maximal tokens for the model:
```
[config]
custom_model_max_tokens= ...
```
(3) Go to [litellm documentation](https://litellm.vercel.app/docs/proxy/quick_start#supported-llms), find the model you want to use, and set the relevant environment variables.

View File

@ -1,7 +1,7 @@
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)**. The different tools and sub-tools used by Qodo Merge 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. 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. See the [Tools Guide](https://qodo-merge-docs.qodo.ai/tools/) for a detailed description of the different tools and their configurations.
There are three ways to set persistent configurations: There are three ways to set persistent configurations:
@ -18,23 +18,29 @@ In terms of precedence, wiki configurations will override local configurations,
## Wiki configuration file 💎 ## 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. `Platforms supported: GitHub, GitLab`
With Qodo Merge 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**. 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**.
![wiki_configuration](https://codium.ai/images/pr_agent/wiki_configuration.png){width=512} ![wiki_configuration](https://codium.ai/images/pr_agent/wiki_configuration.png){width=512}
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. 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 (or \`\`\`toml), to allow better presentation when displayed in the wiki as markdown.
An example content: An example content:
``` ```toml
[pr_description] [pr_description]
generate_ai_title=true generate_ai_title=true
``` ```
PR-Agent will know to remove the triple-quotes when reading the configuration content. Qodo Merge will know to remove the surrounding quotes when reading the configuration content.
## Local configuration file ## Local configuration file
`Platforms supported: GitHub, GitLab, Bitbucket, Azure DevOps`
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. By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect.
For example, if you set in `.pr_agent.toml`: For example, if you set in `.pr_agent.toml`:
@ -53,9 +59,13 @@ Then you can give a list of extra instructions to the `review` tool.
## Global configuration file 💎 ## Global configuration file 💎
`Platforms supported: GitHub, GitLab, Bitbucket`
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. 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. 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`: 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 file [`https://github.com/Codium-ai/pr-agent-settings/.pr_agent.toml`](https://github.com/Codium-ai/pr-agent-settings/blob/main/.pr_agent.toml) 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`. - 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

@ -1,10 +1,11 @@
# Usage guide # Usage guide
This page provides a detailed guide on how to use PR-Agent. It includes information on how to adjust PR-Agent configurations, define which tools will run automatically, manage mail notifications, and other advanced configurations. This page provides a detailed guide on how to use Qodo Merge.
It includes information on how to adjust Qodo Merge configurations, define which tools will run automatically, and other advanced configurations.
- [Introduction](./introduction.md) - [Introduction](./introduction.md)
- [Configuration Options](./configuration_options.md) - [Configuration File](./configuration_options.md)
- [Usage and Automation](./automations_and_usage.md) - [Usage and Automation](./automations_and_usage.md)
- [Local Repo (CLI)](./automations_and_usage.md#local-repo-cli) - [Local Repo (CLI)](./automations_and_usage.md#local-repo-cli)
- [Online Usage](./automations_and_usage.md#online-usage) - [Online Usage](./automations_and_usage.md#online-usage)
@ -14,10 +15,12 @@ This page provides a detailed guide on how to use PR-Agent. It includes informat
- [BitBucket App](./automations_and_usage.md#bitbucket-app) - [BitBucket App](./automations_and_usage.md#bitbucket-app)
- [Azure DevOps Provider](./automations_and_usage.md#azure-devops-provider) - [Azure DevOps Provider](./automations_and_usage.md#azure-devops-provider)
- [Managing Mail Notifications](./mail_notifications.md) - [Managing Mail Notifications](./mail_notifications.md)
- [Changing a Model](./changing_a_model.md)
- [Additional Configurations Walkthrough](./additional_configurations.md) - [Additional Configurations Walkthrough](./additional_configurations.md)
- [Ignoring files from analysis](./additional_configurations.md#ignoring-files-from-analysis) - [Ignoring files from analysis](./additional_configurations.md#ignoring-files-from-analysis)
- [Extra instructions](./additional_configurations.md#extra-instructions) - [Extra instructions](./additional_configurations.md#extra-instructions)
- [Working with large PRs](./additional_configurations.md#working-with-large-prs) - [Working with large PRs](./additional_configurations.md#working-with-large-prs)
- [Changing a model](./additional_configurations.md#changing-a-model) - [Changing a model](./additional_configurations.md#changing-a-model)
- [Patch Extra Lines](./additional_configurations.md#patch-extra-lines) - [Patch Extra Lines](./additional_configurations.md#patch-extra-lines)
- [Editing the prompts](./additional_configurations.md#editing-the-prompts) - [Editing the prompts](./additional_configurations.md#editing-the-prompts)
- [Qodo Merge Pro Models](./PR_agent_pro_models.md)

View File

@ -1,18 +1,13 @@
After [installation](https://codium-ai.github.io/Docs-PR-Agent/installation/), there are three basic ways to invoke CodiumAI PR-Agent: After [installation](https://qodo-merge-docs.qodo.ai/installation/), there are three basic ways to invoke Qodo Merge:
1. Locally running a CLI command 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 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 3. Enabling Qodo Merge 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). Specifically, CLI commands can be issued by invoking a pre-built [docker image](https://qodo-merge-docs.qodo.ai/installation/locally/#using-docker-image), or by invoking a [locally cloned repo](https://qodo-merge-docs.qodo.ai/installation/locally/#run-from-source).
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.
For online usage, you will need to setup either a [GitHub App](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-app) or a [GitHub Action](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action) (GitHub), a [GitLab webhook](https://qodo-merge-docs.qodo.ai/installation/gitlab/#run-a-gitlab-webhook-server) (GitLab), or a [BitBucket App](https://qodo-merge-docs.qodo.ai/installation/bitbucket/#run-using-codiumai-hosted-bitbucket-app) (BitBucket).
**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: These platforms also enable to run Qodo Merge specific tools automatically when a new PR is opened, or on each push to a branch.
`
"github", "gitlab", "bitbucket", "azure", "codecommit", "local", "gerrit"
`

View File

@ -1,15 +1,15 @@
Unfortunately, it is not possible in GitHub to disable mail notifications from a specific user. 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: If you are subscribed to notifications for a repo with Qodo Merge, we recommend turning off notifications for PR comments, to avoid lengthy emails:
![notifications](https://codium.ai/images/pr_agent/notifications.png){width=512} ![notifications](https://codium.ai/images/pr_agent/notifications.png){width=512}
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). As an alternative, you can filter in your mail provider the notifications specifically from the Qodo Merge 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).
![filter_mail_notifications](https://codium.ai/images/pr_agent/filter_mail_notifications.png){width=512} ![filter_mail_notifications](https://codium.ai/images/pr_agent/filter_mail_notifications.png){width=512}
Another option to reduce the mail overload, yet still receive notifications on PR-Agent tools, is to disable the help collapsible section in PR-Agent bot comments. Another option to reduce the mail overload, yet still receive notifications on Qodo Merge tools, is to disable the help collapsible section in Qodo Merge bot comments.
This can done by setting `enable_help_text=false` for the relevant tool in the configuration file. This can done by setting `enable_help_text=false` for the relevant tool in the configuration file.
For example, to disable the help text for the `pr_reviewer` tool, set: For example, to disable the help text for the `pr_reviewer` tool, set:
``` ```

View File

@ -1,25 +1,30 @@
site_name: PR-Agent Documentation site_name: Qodo Merge (formerly known as PR-Agent)
repo_url: https://github.com/Codium-ai/pr-agent repo_url: https://github.com/Codium-ai/pr-agent
repo_name: Codium-ai/pr-agent repo_name: Codium-ai/pr-agent
nav: nav:
- Overview: 'index.md' - Overview:
- Installation: - 'index.md'
- 💎 Qodo Merge Pro: 'overview/pr_agent_pro.md'
- Data Privacy: 'overview/data_privacy.md'
- Installation:
- 'installation/index.md' - 'installation/index.md'
- Locally: 'installation/locally.md' - Locally: 'installation/locally.md'
- GitHub: 'installation/github.md' - GitHub: 'installation/github.md'
- GitLab: 'installation/gitlab.md' - GitLab: 'installation/gitlab.md'
- BitBucket: 'installation/bitbucket.md' - BitBucket: 'installation/bitbucket.md'
- Azure DevOps: 'installation/azure.md' - Azure DevOps: 'installation/azure.md'
- 💎 PR-Agent Pro: 'installation/pr_agent_pro.md' - 💎 Qodo Merge Pro: 'installation/pr_agent_pro.md'
- Usage Guide: - Usage Guide:
- 'usage-guide/index.md' - 'usage-guide/index.md'
- Introduction: 'usage-guide/introduction.md' - Introduction: 'usage-guide/introduction.md'
- Configuration Options: 'usage-guide/configuration_options.md' - Configuration File: 'usage-guide/configuration_options.md'
- Managing Mail Notifications: 'usage-guide/mail_notifications.md'
- Usage and Automation: 'usage-guide/automations_and_usage.md' - Usage and Automation: 'usage-guide/automations_and_usage.md'
- Managing Mail Notifications: 'usage-guide/mail_notifications.md'
- Changing a Model: 'usage-guide/changing_a_model.md'
- Additional Configurations: 'usage-guide/additional_configurations.md' - Additional Configurations: 'usage-guide/additional_configurations.md'
- Tools: - 💎 Qodo Merge Pro Models: 'usage-guide/PR_agent_pro_models'
- Tools:
- 'tools/index.md' - 'tools/index.md'
- Describe: 'tools/describe.md' - Describe: 'tools/describe.md'
- Review: 'tools/review.md' - Review: 'tools/review.md'
@ -36,8 +41,24 @@ nav:
- 💎 Custom Prompt: 'tools/custom_prompt.md' - 💎 Custom Prompt: 'tools/custom_prompt.md'
- 💎 CI Feedback: 'tools/ci_feedback.md' - 💎 CI Feedback: 'tools/ci_feedback.md'
- 💎 Similar Code: 'tools/similar_code.md' - 💎 Similar Code: 'tools/similar_code.md'
- Core Abilities: 'core-abilities/index.md' - Core Abilities:
- Chrome Extension: 'chrome-extension/index.md' - 'core-abilities/index.md'
- Local and global metadata: 'core-abilities/metadata.md'
- Dynamic context: 'core-abilities/dynamic_context.md'
- Self-reflection: 'core-abilities/self_reflection.md'
- Impact evaluation: 'core-abilities/impact_evaluation.md'
- Interactivity: 'core-abilities/interactivity.md'
- Compression strategy: 'core-abilities/compression_strategy.md'
- Code-oriented YAML: 'core-abilities/code_oriented_yaml.md'
- Static code analysis: 'core-abilities/static_code_analysis.md'
- Code Fine-tuning Benchmark: 'finetuning_benchmark/index.md'
- Chrome Extension:
- Qodo Merge Chrome Extension: 'chrome-extension/index.md'
- Features: 'chrome-extension/features.md'
- Data Privacy: 'chrome-extension/data_privacy.md'
- FAQ:
- FAQ: 'faq/index.md'
# - Code Fine-tuning Benchmark: 'finetuning_benchmark/index.md'
theme: theme:
logo: assets/logo.svg logo: assets/logo.svg
@ -57,10 +78,10 @@ theme:
- content.tabs.link - content.tabs.link
- content.code.annotation - content.code.annotation
- content.code.copy - content.code.copy
- toc.integrate - content.tabs.link
language: en language: en
custom_dir: overrides custom_dir: overrides
palette: palette:
- media: "(prefers-color-scheme)" - media: "(prefers-color-scheme)"
toggle: toggle:
@ -69,15 +90,15 @@ theme:
- media: "(prefers-color-scheme: light)" - media: "(prefers-color-scheme: light)"
scheme: default scheme: default
toggle: toggle:
icon: material/toggle-switch-off-outline icon: material/toggle-switch-off-outline
name: Switch to dark mode name: Switch to dark mode
primary: custom primary: custom
accent: custom accent: custom
- media: "(prefers-color-scheme: dark)" - media: "(prefers-color-scheme: dark)"
scheme: slate scheme: slate
toggle: toggle:
icon: material/toggle-switch icon: material/toggle-switch
name: Switch to light mode name: Switch to light mode
primary: custom primary: custom
accent: custom accent: custom
@ -129,6 +150,7 @@ markdown_extensions:
title: On this page title: On this page
toc_depth: 3 toc_depth: 3
permalink: true permalink: true
copyright: | copyright: |
&copy; 2024 <a href="https://www.codium.ai/" target="_blank" rel="noopener">CodiumAI</a> &copy; 2024 <a href="https://www.codium.ai/" target="_blank" rel="noopener">CodiumAI</a>

View File

@ -46,9 +46,10 @@ command2class = {
commands = list(command2class.keys()) commands = list(command2class.keys())
class PRAgent: class PRAgent:
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler): def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.ai_handler = ai_handler # will be initialized in run_action self.ai_handler = ai_handler # will be initialized in run_action
self.forbidden_cli_args = ['enable_auto_approval'] 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:
@ -68,7 +69,9 @@ class PRAgent:
for forbidden_arg in self.forbidden_cli_args: for forbidden_arg in self.forbidden_cli_args:
for arg in args: for arg in args:
if forbidden_arg in arg: if forbidden_arg in arg:
get_logger().error(f"CLI argument for param '{forbidden_arg}' is forbidden. Use instead a configuration file.") get_logger().error(
f"CLI argument for param '{forbidden_arg}' is forbidden. Use instead a configuration file."
)
return False return False
args = update_settings_from_args(args) args = update_settings_from_args(args)
@ -76,7 +79,7 @@ class PRAgent:
if action not in command2class: if action not in command2class:
get_logger().debug(f"Unknown command: {action}") get_logger().debug(f"Unknown command: {action}")
return False return False
with get_logger().contextualize(command=action): with get_logger().contextualize(command=action, pr_url=pr_url):
get_logger().info("PR-Agent request handler started", analytics=True) get_logger().info("PR-Agent request handler started", analytics=True)
if action == "reflect_and_review": if action == "reflect_and_review":
get_settings().pr_reviewer.ask_and_reflect = True get_settings().pr_reviewer.ask_and_reflect = True
@ -94,4 +97,3 @@ class PRAgent:
else: else:
return False return False
return True return True

View File

@ -9,13 +9,20 @@ MAX_TOKENS = {
'gpt-4': 8000, 'gpt-4': 8000,
'gpt-4-0613': 8000, 'gpt-4-0613': 8000,
'gpt-4-32k': 32000, 'gpt-4-32k': 32000,
'gpt-4-1106-preview': 128000, # 128K, but may be limited by config.max_model_tokens '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 'gpt-4-0125-preview': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4o': 128000, # 128K, but may be limited by config.max_model_tokens 'gpt-4o': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4o-2024-05-13': 128000, # 128K, but may be limited by config.max_model_tokens 'gpt-4o-2024-05-13': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4-turbo-preview': 128000, # 128K, but may be limited by config.max_model_tokens 'gpt-4-turbo-preview': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4-turbo-2024-04-09': 128000, # 128K, but may be limited by config.max_model_tokens 'gpt-4-turbo-2024-04-09': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4-turbo': 128000, # 128K, but may be limited by config.max_model_tokens 'gpt-4-turbo': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4o-mini': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4o-mini-2024-07-18': 128000, # 128K, but may be limited by config.max_model_tokens
'gpt-4o-2024-08-06': 128000, # 128K, but may be limited by config.max_model_tokens
'o1-mini': 128000, # 128K, but may be limited by config.max_model_tokens
'o1-mini-2024-09-12': 128000, # 128K, but may be limited by config.max_model_tokens
'o1-preview': 128000, # 128K, but may be limited by config.max_model_tokens
'o1-preview-2024-09-12': 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,
@ -23,17 +30,38 @@ MAX_TOKENS = {
'meta-llama/Llama-2-7b-chat-hf': 4096, 'meta-llama/Llama-2-7b-chat-hf': 4096,
'vertex_ai/codechat-bison': 6144, 'vertex_ai/codechat-bison': 6144,
'vertex_ai/codechat-bison-32k': 32000, 'vertex_ai/codechat-bison-32k': 32000,
'vertex_ai/claude-3-haiku@20240307': 100000,
'vertex_ai/claude-3-sonnet@20240229': 100000,
'vertex_ai/claude-3-opus@20240229': 100000,
'vertex_ai/claude-3-5-sonnet@20240620': 100000,
'vertex_ai/gemini-1.5-pro': 1048576,
'vertex_ai/gemini-1.5-flash': 1048576,
'vertex_ai/gemma2': 8200,
'codechat-bison': 6144, 'codechat-bison': 6144,
'codechat-bison-32k': 32000, 'codechat-bison-32k': 32000,
'anthropic.claude-instant-v1': 100000, 'anthropic.claude-instant-v1': 100000,
'anthropic.claude-v1': 100000, 'anthropic.claude-v1': 100000,
'anthropic.claude-v2': 100000, 'anthropic.claude-v2': 100000,
'anthropic/claude-3-opus-20240229': 100000, 'anthropic/claude-3-opus-20240229': 100000,
'anthropic/claude-3-5-sonnet-20240620': 100000,
'bedrock/anthropic.claude-instant-v1': 100000, 'bedrock/anthropic.claude-instant-v1': 100000,
'bedrock/anthropic.claude-v2': 100000, 'bedrock/anthropic.claude-v2': 100000,
'bedrock/anthropic.claude-v2:1': 100000, 'bedrock/anthropic.claude-v2:1': 100000,
'bedrock/anthropic.claude-3-sonnet-20240229-v1:0': 100000, 'bedrock/anthropic.claude-3-sonnet-20240229-v1:0': 100000,
'bedrock/anthropic.claude-3-haiku-20240307-v1:0': 100000, 'bedrock/anthropic.claude-3-haiku-20240307-v1:0': 100000,
'bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0': 100000,
'claude-3-5-sonnet': 100000,
'groq/llama3-8b-8192': 8192, 'groq/llama3-8b-8192': 8192,
'groq/llama3-70b-8192': 8192, 'groq/llama3-70b-8192': 8192,
'groq/mixtral-8x7b-32768': 32768,
'groq/llama-3.1-8b-instant': 131072,
'groq/llama-3.1-70b-versatile': 131072,
'groq/llama-3.1-405b-reasoning': 131072,
'ollama/llama3': 4096,
'watsonx/meta-llama/llama-3-8b-instruct': 4096,
"watsonx/meta-llama/llama-3-70b-instruct": 4096,
"watsonx/meta-llama/llama-3-405b-instruct": 16384,
"watsonx/ibm/granite-13b-chat-v2": 8191,
"watsonx/ibm/granite-34b-code-instruct": 8191,
"watsonx/mistralai/mistral-large": 32768,
} }

View File

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
class BaseAiHandler(ABC): class BaseAiHandler(ABC):
""" """
This class defines the interface for an AI handler to be used by the PR Agents. This class defines the interface for an AI handler to be used by the PR Agents.
@ -14,7 +15,7 @@ class BaseAiHandler(ABC):
def deployment_id(self): def deployment_id(self):
pass pass
@abstractmethod @abstractmethod
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2, img_path: str = None): async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2, img_path: str = None):
""" """
This method should be implemented to return a chat completion from the AI model. This method should be implemented to return a chat completion from the AI model.
@ -25,4 +26,3 @@ class BaseAiHandler(ABC):
temperature (float): the temperature to use for the chat completion temperature (float): the temperature to use for the chat completion
""" """
pass pass

View File

@ -1,49 +1,32 @@
try: try:
from langchain.chat_models import ChatOpenAI, AzureChatOpenAI from langchain_openai import ChatOpenAI, AzureChatOpenAI
from langchain.schema import SystemMessage, HumanMessage from langchain_core.messages import SystemMessage, HumanMessage
except: # we don't enforce langchain as a dependency, so if it's not installed, just move on except: # we don't enforce langchain as a dependency, so if it's not installed, just move on
pass pass
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler 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
from openai.error import APIError, RateLimitError, Timeout, TryAgain from openai import APIError, RateLimitError, Timeout
from retry import retry from retry import retry
import functools import functools
OPENAI_RETRIES = 5 OPENAI_RETRIES = 5
class LangChainOpenAIHandler(BaseAiHandler): class LangChainOpenAIHandler(BaseAiHandler):
def __init__(self): def __init__(self):
# Initialize OpenAIHandler specific attributes here # Initialize OpenAIHandler specific attributes here
super().__init__() super().__init__()
self.azure = get_settings().get("OPENAI.API_TYPE", "").lower() == "azure" self.azure = get_settings().get("OPENAI.API_TYPE", "").lower() == "azure"
try:
if self.azure: # Create a default unused chat object to trigger early validation
# using a partial function so we can set the deployment_id later to support fallback_deployments self._create_chat(self.deployment_id)
# but still need to access the other settings now so we can raise a proper exception if they're missing
self._chat = functools.partial( def chat(self, messages: list, model: str, temperature: float):
lambda **kwargs: AzureChatOpenAI(**kwargs), chat = self._create_chat(self.deployment_id)
openai_api_key=get_settings().openai.key, return chat.invoke(input=messages, model=model, temperature=temperature)
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 @property
def deployment_id(self): def deployment_id(self):
@ -51,17 +34,43 @@ class LangChainOpenAIHandler(BaseAiHandler):
Returns the deployment ID for the OpenAI API. Returns the deployment ID for the OpenAI API.
""" """
return get_settings().get("OPENAI.DEPLOYMENT_ID", None) return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
@retry(exceptions=(APIError, Timeout, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3)) 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): async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
try: try:
messages=[SystemMessage(content=system), HumanMessage(content=user)] messages = [SystemMessage(content=system), HumanMessage(content=user)]
# get a chat completion from the formatted messages # get a chat completion from the formatted messages
resp = self.chat(messages, model=model, temperature=temperature) resp = self.chat(messages, model=model, temperature=temperature)
finish_reason="completed" finish_reason = "completed"
return resp.content, finish_reason return resp.content, finish_reason
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 e raise e
def _create_chat(self, deployment_id=None):
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
return AzureChatOpenAI(
openai_api_key=get_settings().openai.key,
openai_api_version=get_settings().openai.api_version,
azure_deployment=deployment_id,
azure_endpoint=get_settings().openai.api_base,
)
else:
# for llms that compatible with openai, should use custom api base
openai_api_base = get_settings().get("OPENAI.API_BASE", None)
if openai_api_base is None or len(openai_api_base) == 0:
return ChatOpenAI(openai_api_key=get_settings().openai.key)
else:
return ChatOpenAI(openai_api_key=get_settings().openai.key, openai_api_base=openai_api_base)
except AttributeError as e:
if getattr(e, "name"):
raise ValueError(f"OpenAI {e.name} is required") from e
else:
raise e

View File

@ -1,10 +1,10 @@
import os import os
import requests import requests
import boto3
import litellm import litellm
import openai import openai
from litellm import acompletion from litellm import acompletion
from tenacity import retry, retry_if_exception_type, stop_after_attempt from tenacity import retry, retry_if_exception_type, stop_after_attempt
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler 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
@ -25,12 +25,18 @@ class LiteLLMAIHandler(BaseAiHandler):
Raises a ValueError if the OpenAI key is missing. Raises a ValueError if the OpenAI key is missing.
""" """
self.azure = False self.azure = False
self.aws_bedrock_client = None
self.api_base = None self.api_base = None
self.repetition_penalty = 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
elif 'OPENAI_API_KEY' not in os.environ:
litellm.api_key = "dummy_key"
if get_settings().get("aws.AWS_ACCESS_KEY_ID"):
assert get_settings().aws.AWS_SECRET_ACCESS_KEY and get_settings().aws.AWS_REGION_NAME, "AWS credentials are incomplete"
os.environ["AWS_ACCESS_KEY_ID"] = get_settings().aws.AWS_ACCESS_KEY_ID
os.environ["AWS_SECRET_ACCESS_KEY"] = get_settings().aws.AWS_SECRET_ACCESS_KEY
os.environ["AWS_REGION_NAME"] = get_settings().aws.AWS_REGION_NAME
if get_settings().get("litellm.use_client"): if get_settings().get("litellm.use_client"):
litellm_token = get_settings().get("litellm.LITELLM_TOKEN") litellm_token = get_settings().get("litellm.LITELLM_TOKEN")
assert litellm_token, "LITELLM_TOKEN is required" assert litellm_token, "LITELLM_TOKEN is required"
@ -38,6 +44,12 @@ class LiteLLMAIHandler(BaseAiHandler):
litellm.use_client = True litellm.use_client = True
if get_settings().get("LITELLM.DROP_PARAMS", None): if get_settings().get("LITELLM.DROP_PARAMS", None):
litellm.drop_params = get_settings().litellm.drop_params litellm.drop_params = get_settings().litellm.drop_params
if get_settings().get("LITELLM.SUCCESS_CALLBACK", None):
litellm.success_callback = get_settings().litellm.success_callback
if get_settings().get("LITELLM.FAILURE_CALLBACK", None):
litellm.failure_callback = get_settings().litellm.failure_callback
if get_settings().get("LITELLM.SERVICE_CALLBACK", None):
litellm.service_callback = get_settings().litellm.service_callback
if get_settings().get("OPENAI.ORG", None): if get_settings().get("OPENAI.ORG", None):
litellm.organization = get_settings().openai.org litellm.organization = get_settings().openai.org
if get_settings().get("OPENAI.API_TYPE", None): if get_settings().get("OPENAI.API_TYPE", None):
@ -61,24 +73,16 @@ class LiteLLMAIHandler(BaseAiHandler):
if get_settings().get("HUGGINGFACE.API_BASE", None) and 'huggingface' in get_settings().config.model: 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 self.api_base = get_settings().huggingface.api_base
if get_settings().get("OLLAMA.API_BASE", None) : if get_settings().get("OLLAMA.API_BASE", None):
litellm.api_base = get_settings().ollama.api_base litellm.api_base = get_settings().ollama.api_base
self.api_base = get_settings().ollama.api_base self.api_base = get_settings().ollama.api_base
if get_settings().get("HUGGINGFACE.REPITITION_PENALTY", None): if get_settings().get("HUGGINGFACE.REPETITION_PENALTY", None):
self.repetition_penalty = float(get_settings().huggingface.repetition_penalty) 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(
"VERTEXAI.VERTEX_LOCATION", None "VERTEXAI.VERTEX_LOCATION", None
) )
if get_settings().get("AWS.BEDROCK_REGION", None):
litellm.AmazonAnthropicConfig.max_tokens_to_sample = 2000
litellm.AmazonAnthropicClaude3Config.max_tokens = 2000
self.aws_bedrock_client = boto3.client(
service_name="bedrock-runtime",
region_name=get_settings().aws.bedrock_region,
)
def prepare_logs(self, response, system, user, resp, finish_reason): def prepare_logs(self, response, system, user, resp, finish_reason):
response_log = response.dict().copy() response_log = response.dict().copy()
response_log['system'] = system response_log['system'] = system
@ -91,6 +95,60 @@ class LiteLLMAIHandler(BaseAiHandler):
response_log['main_pr_language'] = 'unknown' response_log['main_pr_language'] = 'unknown'
return response_log return response_log
def add_litellm_callbacks(selfs, kwargs) -> dict:
captured_extra = []
def capture_logs(message):
# Parsing the log message and context
record = message.record
log_entry = {}
if record.get('extra', None).get('command', None) is not None:
log_entry.update({"command": record['extra']["command"]})
if record.get('extra', {}).get('pr_url', None) is not None:
log_entry.update({"pr_url": record['extra']["pr_url"]})
# Append the log entry to the captured_logs list
captured_extra.append(log_entry)
# Adding the custom sink to Loguru
handler_id = get_logger().add(capture_logs)
get_logger().debug("Capturing logs for litellm callbacks")
get_logger().remove(handler_id)
context = captured_extra[0] if len(captured_extra) > 0 else None
command = context.get("command", "unknown")
pr_url = context.get("pr_url", "unknown")
git_provider = get_settings().config.git_provider
metadata = dict()
callbacks = litellm.success_callback + litellm.failure_callback + litellm.service_callback
if "langfuse" in callbacks:
metadata.update({
"trace_name": command,
"tags": [git_provider, command],
"trace_metadata": {
"command": command,
"pr_url": pr_url,
},
})
if "langsmith" in callbacks:
metadata.update({
"run_name": command,
"tags": [git_provider, command],
"extra": {
"metadata": {
"command": command,
"pr_url": pr_url,
}
},
})
# Adding the captured logs to the kwargs
kwargs["metadata"] = metadata
return kwargs
@property @property
def deployment_id(self): def deployment_id(self):
""" """
@ -99,7 +157,7 @@ class LiteLLMAIHandler(BaseAiHandler):
return get_settings().get("OPENAI.DEPLOYMENT_ID", None) return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry( @retry(
retry=retry_if_exception_type((openai.APIError, openai.APIConnectionError, openai.Timeout)), # No retry on RateLimitError retry=retry_if_exception_type((openai.APIError, openai.APIConnectionError, openai.APITimeoutError)), # No retry on RateLimitError
stop=stop_after_attempt(OPENAI_RETRIES) stop=stop_after_attempt(OPENAI_RETRIES)
) )
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2, img_path: str = None): async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2, img_path: str = None):
@ -108,6 +166,10 @@ class LiteLLMAIHandler(BaseAiHandler):
deployment_id = self.deployment_id deployment_id = self.deployment_id
if self.azure: if self.azure:
model = 'azure/' + model model = 'azure/' + model
if 'claude' in model and not system:
system = "No system prompt provided"
get_logger().warning(
"Empty system prompt for claude model. Adding a newline character to prevent OpenAI API error.")
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}] messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
if img_path: if img_path:
try: try:
@ -128,11 +190,20 @@ class LiteLLMAIHandler(BaseAiHandler):
"deployment_id": deployment_id, "deployment_id": deployment_id,
"messages": messages, "messages": messages,
"temperature": temperature, "temperature": temperature,
"force_timeout": get_settings().config.ai_timeout, "timeout": get_settings().config.ai_timeout,
"api_base" : self.api_base, "api_base": self.api_base,
} }
if self.aws_bedrock_client:
kwargs["aws_bedrock_client"] = self.aws_bedrock_client if get_settings().litellm.get("enable_callbacks", False):
kwargs = self.add_litellm_callbacks(kwargs)
seed = get_settings().config.get("seed", -1)
if temperature > 0 and seed >= 0:
raise ValueError(f"Seed ({seed}) is not supported with temperature ({temperature}) > 0")
elif seed >= 0:
get_logger().info(f"Using fixed seed of {seed}")
kwargs["seed"] = seed
if self.repetition_penalty: if self.repetition_penalty:
kwargs["repetition_penalty"] = self.repetition_penalty kwargs["repetition_penalty"] = self.repetition_penalty
@ -143,14 +214,14 @@ class LiteLLMAIHandler(BaseAiHandler):
get_logger().info(f"\nUser prompt:\n{user}") get_logger().info(f"\nUser prompt:\n{user}")
response = await acompletion(**kwargs) response = await acompletion(**kwargs)
except (openai.APIError, openai.Timeout) as e: except (openai.APIError, openai.APITimeoutError) as e:
get_logger().error("Error during OpenAI inference: ", e) get_logger().warning(f"Error during LLM inference: {e}")
raise raise
except (openai.RateLimitError) as e: except (openai.RateLimitError) as e:
get_logger().error("Rate limit error during OpenAI inference: ", e) get_logger().error(f"Rate limit error during LLM inference: {e}")
raise raise
except (Exception) as e: except (Exception) as e:
get_logger().error("Unknown error during OpenAI inference: ", e) get_logger().warning(f"Unknown error during LLM inference: {e}")
raise openai.APIError 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 openai.APIError raise openai.APIError

View File

@ -28,13 +28,14 @@ class OpenAIHandler(BaseAiHandler):
except AttributeError as e: except AttributeError as e:
raise ValueError("OpenAI key is required") from e raise ValueError("OpenAI key is required") from e
@property @property
def deployment_id(self): def deployment_id(self):
""" """
Returns the deployment ID for the OpenAI API. Returns the deployment ID for the OpenAI API.
""" """
return get_settings().get("OPENAI.DEPLOYMENT_ID", None) return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError), @retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3)) 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): async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
@ -54,8 +55,8 @@ class OpenAIHandler(BaseAiHandler):
finish_reason = chat_completion["choices"][0]["finish_reason"] finish_reason = chat_completion["choices"][0]["finish_reason"]
usage = chat_completion.get("usage") usage = chat_completion.get("usage")
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason, get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
model=model, usage=usage) model=model, usage=usage)
return resp, finish_reason return resp, finish_reason
except (APIError, Timeout, TryAgain) as e: except (APIError, Timeout, TryAgain) as e:
get_logger().error("Error during OpenAI inference: ", e) get_logger().error("Error during OpenAI inference: ", e)
raise raise
@ -64,4 +65,4 @@ class OpenAIHandler(BaseAiHandler):
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 TryAgain from e

View File

@ -3,7 +3,8 @@ import re
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
def filter_ignored(files):
def filter_ignored(files, platform = 'github'):
""" """
Filter out files that match the ignore patterns. Filter out files that match the ignore patterns.
""" """
@ -14,7 +15,7 @@ def filter_ignored(files):
if isinstance(patterns, str): if isinstance(patterns, str):
patterns = [patterns] patterns = [patterns]
glob_setting = get_settings().ignore.glob glob_setting = get_settings().ignore.glob
if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py
glob_setting = glob_setting.strip('[]').split(",") glob_setting = glob_setting.strip('[]').split(",")
patterns += [fnmatch.translate(glob) for glob in glob_setting] patterns += [fnmatch.translate(glob) for glob in glob_setting]
@ -27,8 +28,36 @@ def filter_ignored(files):
pass pass
# keep filenames that _don't_ match the ignore regex # keep filenames that _don't_ match the ignore regex
for r in compiled_patterns: if files and isinstance(files, list):
files = [f for f in files if (f.filename and not r.match(f.filename))] for r in compiled_patterns:
if platform == 'github':
files = [f for f in files if (f.filename and not r.match(f.filename))]
elif platform == 'bitbucket':
# files = [f for f in files if (f.new.path and not r.match(f.new.path))]
files_o = []
for f in files:
if hasattr(f, 'new'):
if f.new and f.new.path and not r.match(f.new.path):
files_o.append(f)
continue
if hasattr(f, 'old'):
if f.old and f.old.path and not r.match(f.old.path):
files_o.append(f)
continue
files = files_o
elif platform == 'gitlab':
# files = [f for f in files if (f['new_path'] and not r.match(f['new_path']))]
files_o = []
for f in files:
if 'new_path' in f and f['new_path'] and not r.match(f['new_path']):
files_o.append(f)
continue
if 'old_path' in f and f['old_path'] and not r.match(f['old_path']):
files_o.append(f)
continue
files = files_o
elif platform == 'azure':
files = [f for f in files if not r.match(f)]
except Exception as e: except Exception as e:
print(f"Could not filter file list: {e}") print(f"Could not filter file list: {e}")

View File

@ -1,31 +1,63 @@
from __future__ import annotations from __future__ import annotations
import re import re
import traceback
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
def extend_patch(original_file_str, patch_str, num_lines) -> str: def extend_patch(original_file_str, patch_str, patch_extra_lines_before=0,
""" patch_extra_lines_after=0, filename: str = "") -> str:
Extends the given patch to include a specified number of surrounding lines. if not patch_str or (patch_extra_lines_before == 0 and patch_extra_lines_after == 0) or not original_file_str:
Args:
original_file_str (str): The original file to which the patch will be applied.
patch_str (str): The patch to be applied to the original file.
num_lines (int): The number of surrounding lines to include in the extended patch.
Returns:
str: The extended patch string.
"""
if not patch_str or num_lines == 0:
return patch_str return patch_str
if type(original_file_str) == bytes: original_file_str = decode_if_bytes(original_file_str)
original_file_str = original_file_str.decode('utf-8') if not original_file_str:
return patch_str
if should_skip_patch(filename):
return patch_str
try:
extended_patch_str = process_patch_lines(patch_str, original_file_str,
patch_extra_lines_before, patch_extra_lines_after)
except Exception as e:
get_logger().warning(f"Failed to extend patch: {e}", artifact={"traceback": traceback.format_exc()})
return patch_str
return extended_patch_str
def decode_if_bytes(original_file_str):
if isinstance(original_file_str, bytes):
try:
return original_file_str.decode('utf-8')
except UnicodeDecodeError:
encodings_to_try = ['iso-8859-1', 'latin-1', 'ascii', 'utf-16']
for encoding in encodings_to_try:
try:
return original_file_str.decode(encoding)
except UnicodeDecodeError:
continue
return ""
return original_file_str
def should_skip_patch(filename):
patch_extension_skip_types = get_settings().config.patch_extension_skip_types
if patch_extension_skip_types and filename:
return any(filename.endswith(skip_type) for skip_type in patch_extension_skip_types)
return False
def process_patch_lines(patch_str, original_file_str, patch_extra_lines_before, patch_extra_lines_after):
allow_dynamic_context = get_settings().config.allow_dynamic_context
patch_extra_lines_before_dynamic = get_settings().config.max_extra_lines_before_dynamic_context
original_lines = original_file_str.splitlines() original_lines = original_file_str.splitlines()
len_original_lines = len(original_lines)
patch_lines = patch_str.splitlines() patch_lines = patch_str.splitlines()
extended_patch_lines = [] extended_patch_lines = []
@ -36,47 +68,100 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str:
for line in patch_lines: for line in patch_lines:
if line.startswith('@@'): if line.startswith('@@'):
match = RE_HUNK_HEADER.match(line) match = RE_HUNK_HEADER.match(line)
# identify hunk header
if match: if match:
# finish previous hunk # finish processing previous hunk
if start1 != -1: if start1 != -1 and patch_extra_lines_after > 0:
extended_patch_lines.extend( delta_lines = [f' {line}' for line in original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]]
original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines]) extended_patch_lines.extend(delta_lines)
res = list(match.groups()) section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
for i in range(len(res)):
if res[i] is None: if patch_extra_lines_before > 0 or patch_extra_lines_after > 0:
res[i] = 0 def _calc_context_limits(patch_lines_before):
try: extended_start1 = max(1, start1 - patch_lines_before)
start1, size1, start2, size2 = map(int, res[:4]) extended_size1 = size1 + (start1 - extended_start1) + patch_extra_lines_after
except: # '@@ -0,0 +1 @@' case extended_start2 = max(1, start2 - patch_lines_before)
start1, size1, size2 = map(int, res[:3]) extended_size2 = size2 + (start2 - extended_start2) + patch_extra_lines_after
start2 = 0 if extended_start1 - 1 + extended_size1 > len_original_lines:
section_header = res[4] # we cannot extend beyond the original file
extended_start1 = max(1, start1 - num_lines) delta_cap = extended_start1 - 1 + extended_size1 - len_original_lines
extended_size1 = size1 + (start1 - extended_start1) + num_lines extended_size1 = max(extended_size1 - delta_cap, size1)
extended_start2 = max(1, start2 - num_lines) extended_size2 = max(extended_size2 - delta_cap, size2)
extended_size2 = size2 + (start2 - extended_start2) + num_lines return extended_start1, extended_size1, extended_start2, extended_size2
if allow_dynamic_context:
extended_start1, extended_size1, extended_start2, extended_size2 = \
_calc_context_limits(patch_extra_lines_before_dynamic)
lines_before = original_lines[extended_start1 - 1:start1 - 1]
found_header = False
for i, line, in enumerate(lines_before):
if section_header in line:
found_header = True
# Update start and size in one line each
extended_start1, extended_start2 = extended_start1 + i, extended_start2 + i
extended_size1, extended_size2 = extended_size1 - i, extended_size2 - i
# get_logger().debug(f"Found section header in line {i} before the hunk")
section_header = ''
break
if not found_header:
# get_logger().debug(f"Section header not found in the extra lines before the hunk")
extended_start1, extended_size1, extended_start2, extended_size2 = \
_calc_context_limits(patch_extra_lines_before)
else:
extended_start1, extended_size1, extended_start2, extended_size2 = \
_calc_context_limits(patch_extra_lines_before)
delta_lines = [f' {line}' for line in original_lines[extended_start1 - 1:start1 - 1]]
# logic to remove section header if its in the extra delta lines (in dynamic context, this is also done)
if section_header and not allow_dynamic_context:
for line in delta_lines:
if section_header in line:
section_header = '' # remove section header if it is in the extra delta lines
break
else:
extended_start1 = start1
extended_size1 = size1
extended_start2 = start2
extended_size2 = size2
delta_lines = []
extended_patch_lines.append('')
extended_patch_lines.append( extended_patch_lines.append(
f'@@ -{extended_start1},{extended_size1} ' f'@@ -{extended_start1},{extended_size1} '
f'+{extended_start2},{extended_size2} @@ {section_header}') f'+{extended_start2},{extended_size2} @@ {section_header}')
extended_patch_lines.extend( extended_patch_lines.extend(delta_lines) # one to zero based
original_lines[extended_start1 - 1:start1 - 1]) # one to zero based
continue continue
extended_patch_lines.append(line) extended_patch_lines.append(line)
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 2: get_logger().warning(f"Failed to extend patch: {e}", artifact={"traceback": traceback.format_exc()})
get_logger().error(f"Failed to extend patch: {e}")
return patch_str return patch_str
# finish previous hunk # finish processing last hunk
if start1 != -1: if start1 != -1 and patch_extra_lines_after > 0:
extended_patch_lines.extend( delta_lines = original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]
original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines]) # add space at the beginning of each extra line
delta_lines = [f' {line}' for line in delta_lines]
extended_patch_lines.extend(delta_lines)
extended_patch_str = '\n'.join(extended_patch_lines) extended_patch_str = '\n'.join(extended_patch_lines)
return extended_patch_str return extended_patch_str
def extract_hunk_headers(match):
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
section_header = res[4]
return section_header, size1, size2, start1, start2
def omit_deletion_hunks(patch_lines) -> str: def omit_deletion_hunks(patch_lines) -> str:
""" """
Omit deletion hunks from the patch and return the modified patch. Omit deletion hunks from the patch and return the modified patch.
@ -106,9 +191,10 @@ def omit_deletion_hunks(patch_lines) -> str:
inside_hunk = True inside_hunk = True
else: else:
temp_hunk.append(line) temp_hunk.append(line)
edit_type = line[0] if line:
if edit_type == '+': edit_type = line[0]
add_hunk = True if edit_type == '+':
add_hunk = True
if inside_hunk and add_hunk: if inside_hunk and add_hunk:
added_patched.extend(temp_hunk) added_patched.extend(temp_hunk)
@ -133,7 +219,7 @@ def handle_patch_deletions(patch: str, original_file_content_str: str,
str: The modified patch with deletion hunks omitted. str: The modified patch with deletion hunks omitted.
""" """
if not new_file_content_str and edit_type != EDIT_TYPE.ADDED: if not new_file_content_str and (edit_type == EDIT_TYPE.DELETED or edit_type == EDIT_TYPE.UNKNOWN):
# logic for handling deleted files - don't show patch, just show that the file was deleted # logic for handling deleted files - don't show patch, just show that the file was deleted
if get_settings().config.verbosity_level > 0: if get_settings().config.verbosity_level > 0:
get_logger().info(f"Processing file: {file_name}, minimizing deletion file") get_logger().info(f"Processing file: {file_name}, minimizing deletion file")
@ -180,8 +266,11 @@ __old hunk__
line6 line6
... ...
""" """
# if the file was deleted, return a message indicating that the file was deleted
patch_with_lines_str = f"\n\n## file: '{file.filename.strip()}'\n" if hasattr(file, 'edit_type') and file.edit_type == EDIT_TYPE.DELETED:
return f"\n\n## file '{file.filename.strip()}' was deleted\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+))? @@[ ]?(.*)")
@ -190,66 +279,71 @@ __old hunk__
match = None match = None
start1, size1, start2, size2 = -1, -1, -1, -1 start1, size1, start2, size2 = -1, -1, -1, -1
prev_header_line = [] prev_header_line = []
header_line =[] header_line = []
for line in patch_lines: for line_i, line in enumerate(patch_lines):
if 'no newline at end of file' in line.lower(): if 'no newline at end of file' in line.lower().strip().strip('//'):
continue continue
if line.startswith('@@'): if line.startswith('@@'):
header_line = line header_line = line
match = RE_HUNK_HEADER.match(line) match = RE_HUNK_HEADER.match(line)
if match and new_content_lines: # found a new hunk, split the previous lines if match and (new_content_lines or old_content_lines): # found a new hunk, split the previous lines
if prev_header_line:
patch_with_lines_str += f'\n{prev_header_line}\n'
if new_content_lines: if new_content_lines:
if prev_header_line: is_plus_lines = any([line.startswith('+') for line in new_content_lines])
patch_with_lines_str += f'\n{prev_header_line}\n' if is_plus_lines:
patch_with_lines_str = patch_with_lines_str.rstrip()+'\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 = patch_with_lines_str.rstrip()+'\n__old hunk__\n' is_minus_lines = any([line.startswith('-') for line in old_content_lines])
for line_old in old_content_lines: if is_minus_lines:
patch_with_lines_str += f"{line_old}\n" patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n"
new_content_lines = [] new_content_lines = []
old_content_lines = [] old_content_lines = []
if match: if match:
prev_header_line = header_line prev_header_line = header_line
res = list(match.groups()) section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
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
elif line.startswith('+'): elif line.startswith('+'):
new_content_lines.append(line) new_content_lines.append(line)
elif line.startswith('-'): elif line.startswith('-'):
old_content_lines.append(line) old_content_lines.append(line)
else: else:
if not line and line_i: # if this line is empty and the next line is a hunk header, skip it
if line_i + 1 < len(patch_lines) and patch_lines[line_i + 1].startswith('@@'):
continue
elif line_i + 1 == len(patch_lines):
continue
new_content_lines.append(line) new_content_lines.append(line)
old_content_lines.append(line) old_content_lines.append(line)
# finishing last hunk # finishing last hunk
if match and new_content_lines: if match and new_content_lines:
patch_with_lines_str += f'\n{header_line}\n'
if new_content_lines: if new_content_lines:
patch_with_lines_str += f'\n{header_line}\n' is_plus_lines = any([line.startswith('+') for line in new_content_lines])
patch_with_lines_str = patch_with_lines_str.rstrip()+ '\n__new hunk__\n' if is_plus_lines:
for i, line_new in enumerate(new_content_lines): patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__new hunk__\n'
patch_with_lines_str += f"{start2 + i} {line_new}\n" for i, line_new in enumerate(new_content_lines):
patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines: if old_content_lines:
patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n' is_minus_lines = any([line.startswith('-') for line in old_content_lines])
for line_old in old_content_lines: if is_minus_lines:
patch_with_lines_str += f"{line_old}\n" patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
for line_old in old_content_lines:
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]: 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" patch_with_lines_str = f"\n\n## File: '{file_name.strip()}'\n\n"
selected_lines = "" selected_lines = ""
patch_lines = patch.splitlines() patch_lines = patch.splitlines()
RE_HUNK_HEADER = re.compile( RE_HUNK_HEADER = re.compile(
@ -269,15 +363,7 @@ def extract_hunk_lines_from_patch(patch: str, file_name, line_start, line_end, s
match = RE_HUNK_HEADER.match(line) match = RE_HUNK_HEADER.match(line)
res = list(match.groups()) section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
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 # check if line range is in this hunk
if side.lower() == 'left': if side.lower() == 'left':

View File

@ -5,17 +5,22 @@ from pr_agent.config_loader import get_settings
# Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501
bad_extensions = get_settings().bad_extensions.default
if get_settings().config.use_extra_bad_extensions:
bad_extensions += get_settings().bad_extensions.extra
def filter_bad_extensions(files): def filter_bad_extensions(files):
return [f for f in files if f.filename is not None and is_valid_file(f.filename)] # Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501
bad_extensions = get_settings().bad_extensions.default
if get_settings().config.use_extra_bad_extensions:
bad_extensions += get_settings().bad_extensions.extra
return [f for f in files if f.filename is not None and is_valid_file(f.filename, bad_extensions)]
def is_valid_file(filename): def is_valid_file(filename:str, bad_extensions=None) -> bool:
if not filename:
return False
if not bad_extensions:
bad_extensions = get_settings().bad_extensions.default
if get_settings().config.use_extra_bad_extensions:
bad_extensions += get_settings().bad_extensions.extra
return filename.split('.')[-1] not in bad_extensions return filename.split('.')[-1] not in bad_extensions

View File

@ -21,33 +21,32 @@ MORE_MODIFIED_FILES_ = "Additional modified files (insufficient token budget to
ADDED_FILES_ = "Additional added files (insufficient token budget to process):\n" ADDED_FILES_ = "Additional added files (insufficient token budget to process):\n"
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000 OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1500
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600 OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 1000
MAX_EXTRA_LINES = 10
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: str,
add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False) -> str:
"""
Returns a string with the diff of the pull request, applying diff minimization techniques if needed.
Args: def cap_and_log_extra_lines(value, direction) -> int:
git_provider (GitProvider): An object of the GitProvider class representing the Git provider used for the pull if value > MAX_EXTRA_LINES:
request. get_logger().warning(f"patch_extra_lines_{direction} was {value}, capping to {MAX_EXTRA_LINES}")
token_handler (TokenHandler): An object of the TokenHandler class used for handling tokens in the context of the return MAX_EXTRA_LINES
pull request. return value
model (str): The name of the model used for tokenization.
add_line_numbers_to_hunks (bool, optional): A boolean indicating whether to add line numbers to the hunks in the
diff. Defaults to False.
disable_extra_lines (bool, optional): A boolean indicating whether to disable the extension of each patch with
extra lines of context. Defaults to False.
Returns:
str: A string with the diff of the pull request, applying diff minimization techniques if needed.
"""
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
model: str,
add_line_numbers_to_hunks: bool = False,
disable_extra_lines: bool = False,
large_pr_handling=False,
return_remaining_files=False):
if disable_extra_lines: if disable_extra_lines:
PATCH_EXTRA_LINES = 0 PATCH_EXTRA_LINES_BEFORE = 0
PATCH_EXTRA_LINES_AFTER = 0
else: else:
PATCH_EXTRA_LINES = get_settings().config.patch_extra_lines PATCH_EXTRA_LINES_BEFORE = get_settings().config.patch_extra_lines_before
PATCH_EXTRA_LINES_AFTER = get_settings().config.patch_extra_lines_after
PATCH_EXTRA_LINES_BEFORE = cap_and_log_extra_lines(PATCH_EXTRA_LINES_BEFORE, "before")
PATCH_EXTRA_LINES_AFTER = cap_and_log_extra_lines(PATCH_EXTRA_LINES_AFTER, "after")
try: try:
diff_files_original = git_provider.get_diff_files() diff_files_original = git_provider.get_diff_files()
@ -76,7 +75,8 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
# 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(
pr_languages, token_handler, add_line_numbers_to_hunks, patch_extra_lines=PATCH_EXTRA_LINES) pr_languages, token_handler, add_line_numbers_to_hunks,
patch_extra_lines_before=PATCH_EXTRA_LINES_BEFORE, patch_extra_lines_after=PATCH_EXTRA_LINES_AFTER)
# 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):
@ -84,56 +84,110 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
f"returning full diff.") 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 (If we got here, we will not extend the patches with extra lines)
get_logger().info(f"Tokens: {total_tokens}, total tokens over limit: {get_max_tokens(model)}, " get_logger().info(f"Tokens: {total_tokens}, total tokens over limit: {get_max_tokens(model)}, "
f"pruning diff.") f"pruning diff.")
patches_compressed, modified_file_names, deleted_file_names, added_file_names, total_tokens_new = \ patches_compressed_list, total_tokens_list, deleted_files_list, remaining_files_list, file_dict, files_in_patches_list = \
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, large_pr_handling)
if large_pr_handling and len(patches_compressed_list) > 1:
get_logger().info(f"Large PR handling mode, and found {len(patches_compressed_list)} patches with original diff.")
return "" # return empty string, as we want to generate multiple patches with a different prompt
# return the first patch
patches_compressed = patches_compressed_list[0]
total_tokens_new = total_tokens_list[0]
files_in_patch = files_in_patches_list[0]
# Insert additional information about added, modified, and deleted files if there is enough space # Insert additional information about added, modified, and deleted files if there is enough space
max_tokens = get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD max_tokens = get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD
curr_token = total_tokens_new # == token_handler.count_tokens(final_diff)+token_handler.prompt_tokens curr_token = total_tokens_new # == token_handler.count_tokens(final_diff)+token_handler.prompt_tokens
final_diff = "\n".join(patches_compressed) final_diff = "\n".join(patches_compressed)
delta_tokens = 10 delta_tokens = 10
if added_file_names and (max_tokens - curr_token) > delta_tokens: added_list_str = modified_list_str = deleted_list_str = ""
added_list_str = ADDED_FILES_ + "\n".join(added_file_names) unprocessed_files = []
added_list_str = clip_tokens(added_list_str, max_tokens - curr_token) # generate the added, modified, and deleted files lists
if added_list_str: if (max_tokens - curr_token) > delta_tokens:
final_diff = final_diff + "\n\n" + added_list_str for filename, file_values in file_dict.items():
curr_token += token_handler.count_tokens(added_list_str) + 2 if filename in files_in_patch:
if modified_file_names and (max_tokens - curr_token) > delta_tokens: continue
modified_list_str = MORE_MODIFIED_FILES_ + "\n".join(modified_file_names) if file_values['edit_type'] == EDIT_TYPE.ADDED:
modified_list_str = clip_tokens(modified_list_str, max_tokens - curr_token) unprocessed_files.append(filename)
if modified_list_str: if not added_list_str:
final_diff = final_diff + "\n\n" + modified_list_str added_list_str = ADDED_FILES_ + f"\n{filename}"
curr_token += token_handler.count_tokens(modified_list_str) + 2 else:
if deleted_file_names and (max_tokens - curr_token) > delta_tokens: added_list_str = added_list_str + f"\n{filename}"
deleted_list_str = DELETED_FILES_ + "\n".join(deleted_file_names) elif file_values['edit_type'] in [EDIT_TYPE.MODIFIED, EDIT_TYPE.RENAMED]:
deleted_list_str = clip_tokens(deleted_list_str, max_tokens - curr_token) unprocessed_files.append(filename)
if deleted_list_str: if not modified_list_str:
final_diff = final_diff + "\n\n" + deleted_list_str modified_list_str = MORE_MODIFIED_FILES_ + f"\n{filename}"
else:
modified_list_str = modified_list_str + f"\n{filename}"
elif file_values['edit_type'] == EDIT_TYPE.DELETED:
# unprocessed_files.append(filename) # not needed here, because the file was deleted, so no need to process it
if not deleted_list_str:
deleted_list_str = DELETED_FILES_ + f"\n{filename}"
else:
deleted_list_str = deleted_list_str + f"\n{filename}"
# prune the added, modified, and deleted files lists, and add them to the final diff
added_list_str = clip_tokens(added_list_str, max_tokens - curr_token)
if added_list_str:
final_diff = final_diff + "\n\n" + added_list_str
curr_token += token_handler.count_tokens(added_list_str) + 2
modified_list_str = clip_tokens(modified_list_str, max_tokens - curr_token)
if modified_list_str:
final_diff = final_diff + "\n\n" + modified_list_str
curr_token += token_handler.count_tokens(modified_list_str) + 2
deleted_list_str = clip_tokens(deleted_list_str, max_tokens - curr_token)
if deleted_list_str:
final_diff = final_diff + "\n\n" + deleted_list_str
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}")
if not return_remaining_files:
return final_diff
else:
return final_diff, remaining_files_list
def get_pr_diff_multiple_patchs(git_provider: GitProvider, token_handler: TokenHandler, model: str,
add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False):
try: try:
get_logger().debug(f"After pruning, added_list_str: {added_list_str}, modified_list_str: {modified_list_str}, " diff_files_original = git_provider.get_diff_files()
f"deleted_list_str: {deleted_list_str}") except RateLimitExceededException as e:
except Exception as e: get_logger().error(f"Rate limit exceeded for git provider API. original message {e}")
pass raise
return final_diff
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
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
patches_compressed_list, total_tokens_list, deleted_files_list, remaining_files_list, file_dict, files_in_patches_list = \
pr_generate_compressed_diff(pr_languages, token_handler, model, add_line_numbers_to_hunks, large_pr_handling=True)
return patches_compressed_list, total_tokens_list, deleted_files_list, remaining_files_list, file_dict, files_in_patches_list
def pr_generate_extended_diff(pr_languages: list, def pr_generate_extended_diff(pr_languages: list,
token_handler: TokenHandler, token_handler: TokenHandler,
add_line_numbers_to_hunks: bool, add_line_numbers_to_hunks: bool,
patch_extra_lines: int = 0) -> Tuple[list, int, list]: patch_extra_lines_before: int = 0,
""" patch_extra_lines_after: int = 0) -> Tuple[list, int, list]:
Generate a standard diff string with patch extension, while counting the number of tokens used and applying diff
minimization techniques if needed.
Args:
- pr_languages: A list of dictionaries representing the languages used in the pull request and their corresponding
files.
- token_handler: An object of the TokenHandler class used for handling tokens in the context of the pull request.
- add_line_numbers_to_hunks: A boolean indicating whether to add line numbers to the hunks in the diff.
"""
total_tokens = token_handler.prompt_tokens # initial tokens total_tokens = token_handler.prompt_tokens # initial tokens
patches_extended = [] patches_extended = []
patches_extended_tokens = [] patches_extended_tokens = []
@ -145,12 +199,20 @@ def pr_generate_extended_diff(pr_languages: list,
continue continue
# extend each patch with extra lines of context # extend each patch with extra lines of context
extended_patch = extend_patch(original_file_content_str, patch, num_lines=patch_extra_lines) extended_patch = extend_patch(original_file_content_str, patch,
full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n" patch_extra_lines_before, patch_extra_lines_after, file.filename)
if not extended_patch:
get_logger().warning(f"Failed to extend patch for file: {file.filename}")
continue
full_extended_patch = f"\n\n## {file.filename}\n{extended_patch.rstrip()}\n"
if add_line_numbers_to_hunks: if add_line_numbers_to_hunks:
full_extended_patch = convert_to_hunks_with_lines_numbers(extended_patch, file) full_extended_patch = convert_to_hunks_with_lines_numbers(extended_patch, file)
# add AI-summary metadata to the patch
if file.ai_file_summary and get_settings().get("config.enable_ai_metadata", False):
full_extended_patch = add_ai_summary_top_patch(file, full_extended_patch)
patch_tokens = token_handler.count_tokens(full_extended_patch) patch_tokens = token_handler.count_tokens(full_extended_patch)
file.tokens = patch_tokens file.tokens = patch_tokens
total_tokens += patch_tokens total_tokens += patch_tokens
@ -161,41 +223,17 @@ def pr_generate_extended_diff(pr_languages: list,
def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, model: str, def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, model: str,
convert_hunks_to_line_numbers: bool) -> Tuple[list, list, list, list, int]: convert_hunks_to_line_numbers: bool,
""" large_pr_handling: bool) -> Tuple[list, list, list, list, dict, list]:
Generate a compressed diff string for a pull request, using diff minimization techniques to reduce the number of
tokens used.
Args:
top_langs (list): A list of dictionaries representing the languages used in the pull request and their
corresponding files.
token_handler (TokenHandler): An object of the TokenHandler class used for handling tokens in the context of the
pull request.
model (str): The model used for tokenization.
convert_hunks_to_line_numbers (bool): A boolean indicating whether to convert hunks to line numbers in the diff.
Returns:
Tuple[list, list, list]: A tuple containing the following lists:
- patches: A list of compressed diff patches for each file in the pull request.
- modified_files_list: A list of file names that were skipped due to large patch size.
- deleted_files_list: A list of file names that were deleted in the pull request.
Minimization techniques to reduce the number of tokens:
0. Start from the largest diff patch to smaller ones
1. Don't use extend context lines around diff
2. Minimize deleted files
3. Minimize deleted hunks
4. Minimize all remaining files when you reach token limit
"""
patches = []
added_files_list = []
modified_files_list = []
deleted_files_list = [] deleted_files_list = []
# sort each one of the languages in top_langs by the number of tokens in the diff # sort each one of the languages in top_langs by the number of tokens in the diff
sorted_files = [] sorted_files = []
for lang in top_langs: for lang in top_langs:
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))
total_tokens = token_handler.prompt_tokens # generate patches for each file, and count tokens
file_dict = {}
for file in sorted_files: for file in sorted_files:
original_file_content_str = file.base_file original_file_content_str = file.base_file
new_file_content_str = file.head_file new_file_content_str = file.head_file
@ -207,55 +245,91 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
patch = handle_patch_deletions(patch, original_file_content_str, patch = handle_patch_deletions(patch, original_file_content_str,
new_file_content_str, file.filename, file.edit_type) new_file_content_str, file.filename, file.edit_type)
if patch is None: if patch is None:
# if not deleted_files_list:
# total_tokens += token_handler.count_tokens(DELETED_FILES_)
if file.filename not in deleted_files_list: if file.filename not in deleted_files_list:
deleted_files_list.append(file.filename) deleted_files_list.append(file.filename)
# total_tokens += token_handler.count_tokens(file.filename) + 1
continue continue
if convert_hunks_to_line_numbers: if convert_hunks_to_line_numbers:
patch = convert_to_hunks_with_lines_numbers(patch, file) patch = convert_to_hunks_with_lines_numbers(patch, file)
## add AI-summary metadata to the patch (disabled, since we are in the compressed diff)
# if file.ai_file_summary and get_settings().config.get('config.is_auto_command', False):
# patch = add_ai_summary_top_patch(file, patch)
new_patch_tokens = token_handler.count_tokens(patch) new_patch_tokens = token_handler.count_tokens(patch)
file_dict[file.filename] = {'patch': patch, 'tokens': new_patch_tokens, 'edit_type': file.edit_type}
max_tokens_model = get_max_tokens(model)
# first iteration
files_in_patches_list = []
remaining_files_list = [file.filename for file in sorted_files]
patches_list =[]
total_tokens_list = []
total_tokens, patches, remaining_files_list, files_in_patch_list = generate_full_patch(convert_hunks_to_line_numbers, file_dict,
max_tokens_model, remaining_files_list, token_handler)
patches_list.append(patches)
total_tokens_list.append(total_tokens)
files_in_patches_list.append(files_in_patch_list)
# additional iterations (if needed)
if large_pr_handling:
NUMBER_OF_ALLOWED_ITERATIONS = get_settings().pr_description.max_ai_calls - 1 # one more call is to summarize
for i in range(NUMBER_OF_ALLOWED_ITERATIONS-1):
if remaining_files_list:
total_tokens, patches, remaining_files_list, files_in_patch_list = generate_full_patch(convert_hunks_to_line_numbers,
file_dict,
max_tokens_model,
remaining_files_list, token_handler)
if patches:
patches_list.append(patches)
total_tokens_list.append(total_tokens)
files_in_patches_list.append(files_in_patch_list)
else:
break
return patches_list, total_tokens_list, deleted_files_list, remaining_files_list, file_dict, files_in_patches_list
def generate_full_patch(convert_hunks_to_line_numbers, file_dict, max_tokens_model,remaining_files_list_prev, token_handler):
total_tokens = token_handler.prompt_tokens # initial tokens
patches = []
remaining_files_list_new = []
files_in_patch_list = []
for filename, data in file_dict.items():
if filename not in remaining_files_list_prev:
continue
patch = data['patch']
new_patch_tokens = data['tokens']
edit_type = data['edit_type']
# Hard Stop, no more tokens # Hard Stop, no more tokens
if total_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD: if total_tokens > max_tokens_model - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD:
get_logger().warning(f"File was fully skipped, no more tokens: {file.filename}.") get_logger().warning(f"File was fully skipped, no more tokens: {filename}.")
continue continue
# If the patch is too large, just show the file name # If the patch is too large, just show the file name
if total_tokens + new_patch_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD: if total_tokens + new_patch_tokens > max_tokens_model - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD:
# Current logic is to skip the patch if it's too large # Current logic is to skip the patch if it's too large
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens # TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
# until we meet the requirements # until we meet the requirements
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().warning(f"Patch too large, minimizing it, {file.filename}") get_logger().warning(f"Patch too large, skipping it, {filename}")
if file.edit_type == EDIT_TYPE.ADDED: remaining_files_list_new.append(filename)
# if not added_files_list:
# total_tokens += token_handler.count_tokens(ADDED_FILES_)
if file.filename not in added_files_list:
added_files_list.append(file.filename)
# total_tokens += token_handler.count_tokens(file.filename) + 1
else:
# if not modified_files_list:
# total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_)
if file.filename not in modified_files_list:
modified_files_list.append(file.filename)
# total_tokens += token_handler.count_tokens(file.filename) + 1
continue continue
if patch: if patch:
if not convert_hunks_to_line_numbers: if not convert_hunks_to_line_numbers:
patch_final = f"\n\n## file: '{file.filename.strip()}\n\n{patch.strip()}\n'" patch_final = f"\n\n## File: '{filename.strip()}\n\n{patch.strip()}\n'"
else: else:
patch_final = "\n\n" + patch.strip() 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)
files_in_patch_list.append(filename)
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}") get_logger().info(f"Tokens: {total_tokens}, last filename: {filename}")
return total_tokens, patches, remaining_files_list_new, files_in_patch_list
return patches, modified_files_list, deleted_files_list, added_files_list, total_tokens
async def retry_with_fallback_models(f: Callable, model_type: ModelType = ModelType.REGULAR): async def retry_with_fallback_models(f: Callable, model_type: ModelType = ModelType.REGULAR):
@ -273,11 +347,9 @@ async def retry_with_fallback_models(f: Callable, model_type: ModelType = ModelT
except: 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"{traceback.format_exc()}"
) )
if i == len(all_models) - 1: # If it's the last iteration if i == len(all_models) - 1: # If it's the last iteration
raise # Re-raise the last exception raise Exception(f"Failed to generate prediction with any model of {all_models}")
def _get_all_models(model_type: ModelType = ModelType.REGULAR) -> List[str]: def _get_all_models(model_type: ModelType = ModelType.REGULAR) -> List[str]:
@ -343,12 +415,21 @@ 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))
# Get the maximum number of extra lines before and after the patch
PATCH_EXTRA_LINES_BEFORE = get_settings().config.patch_extra_lines_before
PATCH_EXTRA_LINES_AFTER = get_settings().config.patch_extra_lines_after
PATCH_EXTRA_LINES_BEFORE = cap_and_log_extra_lines(PATCH_EXTRA_LINES_BEFORE, "before")
PATCH_EXTRA_LINES_AFTER = cap_and_log_extra_lines(PATCH_EXTRA_LINES_AFTER, "after")
# try first a single run with standard diff string, with patch extension, and no deletions # 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( patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(
pr_languages, token_handler, add_line_numbers_to_hunks=True) pr_languages, token_handler, add_line_numbers_to_hunks=True,
patch_extra_lines_before=PATCH_EXTRA_LINES_BEFORE,
patch_extra_lines_after=PATCH_EXTRA_LINES_AFTER)
# 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):
return ["\n".join(patches_extended)] return ["\n".join(patches_extended)] if patches_extended else []
patches = [] patches = []
final_diff_list = [] final_diff_list = []
@ -372,11 +453,30 @@ def get_pr_multi_diffs(git_provider: GitProvider,
continue continue
patch = convert_to_hunks_with_lines_numbers(patch, file) patch = convert_to_hunks_with_lines_numbers(patch, file)
# add AI-summary metadata to the patch
if file.ai_file_summary and get_settings().get("config.enable_ai_metadata", False):
patch = add_ai_summary_top_patch(file, patch)
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: if patch and (token_handler.prompt_tokens + new_patch_tokens) > get_max_tokens(
get_logger().warning(f"Patch too large, skipping: {file.filename}") model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD:
continue if get_settings().config.get('large_patch_policy', 'skip') == 'skip':
get_logger().warning(f"Patch too large, skipping: {file.filename}")
continue
elif get_settings().config.get('large_patch_policy') == 'clip':
delta_tokens = get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD - token_handler.prompt_tokens
patch_clipped = clip_tokens(patch, delta_tokens, delete_last_line=True, num_input_tokens=new_patch_tokens)
new_patch_tokens = token_handler.count_tokens(patch_clipped)
if patch_clipped 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
else:
get_logger().info(f"Clipped large patch for file: {file.filename}")
patch = patch_clipped
else:
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)
@ -384,6 +484,10 @@ def get_pr_multi_diffs(git_provider: GitProvider,
patches = [] patches = []
total_tokens = token_handler.prompt_tokens total_tokens = token_handler.prompt_tokens
call_number += 1 call_number += 1
if call_number > max_calls: # avoid creating new patches
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Reached max calls ({max_calls})")
break
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Call number: {call_number}") get_logger().info(f"Call number: {call_number}")
@ -398,4 +502,47 @@ def get_pr_multi_diffs(git_provider: GitProvider,
final_diff = "\n".join(patches) final_diff = "\n".join(patches)
final_diff_list.append(final_diff) final_diff_list.append(final_diff)
return final_diff_list return final_diff_list
def add_ai_metadata_to_diff_files(git_provider, pr_description_files):
"""
Adds AI metadata to the diff files based on the PR description files (FilePatchInfo.ai_file_summary).
"""
try:
if not pr_description_files:
get_logger().warning(f"PR description files are empty.")
return
available_files = {pr_file['full_file_name'].strip(): pr_file for pr_file in pr_description_files}
diff_files = git_provider.get_diff_files()
found_any_match = False
for file in diff_files:
filename = file.filename.strip()
if filename in available_files:
file.ai_file_summary = available_files[filename]
found_any_match = True
if not found_any_match:
get_logger().error(f"Failed to find any matching files between PR description and diff files.",
artifact={"pr_description_files": pr_description_files})
except Exception as e:
get_logger().error(f"Failed to add AI metadata to diff files: {e}",
artifact={"traceback": traceback.format_exc()})
def add_ai_summary_top_patch(file, full_extended_patch):
try:
# below every instance of '## File: ...' in the patch, add the ai-summary metadata
full_extended_patch_lines = full_extended_patch.split("\n")
for i, line in enumerate(full_extended_patch_lines):
if line.startswith("## File:") or line.startswith("## file:"):
full_extended_patch_lines.insert(i + 1,
f"### AI-generated changes summary:\n{file.ai_file_summary['long_summary']}")
full_extended_patch = "\n".join(full_extended_patch_lines)
return full_extended_patch
# if no '## File: ...' was found
return full_extended_patch
except Exception as e:
get_logger().error(f"Failed to add AI summary to the top of the patch: {e}",
artifact={"traceback": traceback.format_exc()})
return full_extended_patch

View File

@ -3,6 +3,8 @@ from tiktoken import encoding_for_model, get_encoding
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from threading import Lock from threading import Lock
from pr_agent.log import get_logger
class TokenEncoder: class TokenEncoder:
_encoder_instance = None _encoder_instance = None
@ -62,12 +64,16 @@ class TokenHandler:
Returns: Returns:
The sum of the number of tokens in the system and user strings. The sum of the number of tokens in the system and user strings.
""" """
environment = Environment(undefined=StrictUndefined) try:
system_prompt = environment.from_string(system).render(vars) environment = Environment(undefined=StrictUndefined)
user_prompt = environment.from_string(user).render(vars) system_prompt = environment.from_string(system).render(vars)
system_prompt_tokens = len(encoder.encode(system_prompt)) user_prompt = environment.from_string(user).render(vars)
user_prompt_tokens = len(encoder.encode(user_prompt)) system_prompt_tokens = len(encoder.encode(system_prompt))
return system_prompt_tokens + user_prompt_tokens user_prompt_tokens = len(encoder.encode(user_prompt))
return system_prompt_tokens + user_prompt_tokens
except Exception as e:
get_logger().error(f"Error in _get_system_user_tokens: {e}")
return 0
def count_tokens(self, patch: str) -> int: def count_tokens(self, patch: str) -> int:
""" """

View File

@ -21,3 +21,4 @@ class FilePatchInfo:
old_filename: str = None old_filename: str = None
num_plus_lines: int = -1 num_plus_lines: int = -1
num_minus_lines: int = -1 num_minus_lines: int = -1
ai_file_summary: str = None

View File

@ -1,15 +1,20 @@
from __future__ import annotations from __future__ import annotations
import html2text
import html
import copy
import difflib import difflib
import json import json
import os import os
import re import re
import textwrap import textwrap
import time
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Any, List, Tuple from typing import Any, List, Tuple
import yaml import yaml
from pydantic import BaseModel
from starlette_context import context from starlette_context import context
from pr_agent.algo import MAX_TOKENS from pr_agent.algo import MAX_TOKENS
@ -18,10 +23,22 @@ from pr_agent.config_loader import get_settings, global_settings
from pr_agent.algo.types import FilePatchInfo from pr_agent.algo.types import FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
class Range(BaseModel):
line_start: int # should be 0-indexed
line_end: int
column_start: int = -1
column_end: int = -1
class ModelType(str, Enum): class ModelType(str, Enum):
REGULAR = "regular" REGULAR = "regular"
TURBO = "turbo" TURBO = "turbo"
class PRReviewHeader(str, Enum):
REGULAR = "## PR Reviewer Guide"
INCREMENTAL = "## Incremental PR Reviewer Guide"
def get_setting(key: str) -> Any: def get_setting(key: str) -> Any:
try: try:
key = key.upper() key = key.upper()
@ -30,7 +47,7 @@ def get_setting(key: str) -> Any:
return global_settings.get(key, None) return global_settings.get(key, None)
def emphasize_header(text: str) -> str: def emphasize_header(text: str, only_markdown=False, reference_link=None) -> str:
try: try:
# Finding the position of the first occurrence of ": " # Finding the position of the first occurrence of ": "
colon_position = text.find(": ") colon_position = text.find(": ")
@ -38,7 +55,16 @@ def emphasize_header(text: str) -> str:
# Splitting the string and wrapping the first part in <strong> tags # Splitting the string and wrapping the first part in <strong> tags
if colon_position != -1: if colon_position != -1:
# Everything before the colon (inclusive) is wrapped in <strong> tags # Everything before the colon (inclusive) is wrapped in <strong> tags
transformed_string = "<strong>" + text[:colon_position + 1] + "</strong>" + text[colon_position + 1:] if only_markdown:
if reference_link:
transformed_string = f"[**{text[:colon_position + 1]}**]({reference_link})\n" + text[colon_position + 1:]
else:
transformed_string = f"**{text[:colon_position + 1]}**\n" + text[colon_position + 1:]
else:
if reference_link:
transformed_string = f"<strong><a href='{reference_link}'>{text[:colon_position + 1]}</a></strong><br>" + text[colon_position + 1:]
else:
transformed_string = "<strong>" + text[:colon_position + 1] + "</strong>" +'<br>' + text[colon_position + 1:]
else: else:
# If there's no ": ", return the original string # If there's no ": ", return the original string
transformed_string = text transformed_string = text
@ -60,8 +86,10 @@ def unique_strings(input_list: List[str]) -> List[str]:
seen.add(item) seen.add(item)
return unique_list return unique_list
def convert_to_markdown_v2(output_data: dict,
def convert_to_markdown(output_data: dict, gfm_supported: bool = True, incremental_review=None) -> str: gfm_supported: bool = True,
incremental_review=None,
git_provider=None) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
Args: Args:
@ -72,10 +100,12 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool = True, increment
emojis = { emojis = {
"Can be split": "🔀", "Can be split": "🔀",
"Possible issues": "", "Key issues to review": "",
"Recommended focus areas for review": "",
"Score": "🏅", "Score": "🏅",
"Relevant tests": "🧪", "Relevant tests": "🧪",
"Focused PR": "", "Focused PR": "",
"Relevant ticket": "🎫",
"Security concerns": "🔒", "Security concerns": "🔒",
"Insights from user's answers": "📝", "Insights from user's answers": "📝",
"Code feedback": "🤖", "Code feedback": "🤖",
@ -83,58 +113,125 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool = True, increment
} }
markdown_text = "" markdown_text = ""
if not incremental_review: if not incremental_review:
markdown_text += f"## PR Review 🔍\n\n" markdown_text += f"{PRReviewHeader.REGULAR.value} 🔍\n\n"
else: else:
markdown_text += f"## Incremental PR Review 🔍 \n\n" markdown_text += f"{PRReviewHeader.INCREMENTAL.value} 🔍\n\n"
markdown_text += f"⏮️ Review for commits since previous PR-Agent review {incremental_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>"""
if not output_data or not output_data.get('review', {}): if not output_data or not output_data.get('review', {}):
return "" return ""
if get_settings().get("pr_reviewer.enable_intro_text", False):
markdown_text += f"Here are some key observations to aid the review process:\n\n"
if gfm_supported:
markdown_text += "<table>\n"
for key, value in output_data['review'].items(): for key, value in output_data['review'].items():
if value is None or value == '' or value == {} or value == []: if value is None or value == '' or value == {} or value == []:
if key.lower() != 'can_be_split': if key.lower() not in ['can_be_split', 'key_issues_to_review']:
continue continue
key_nice = key.replace('_', ' ').capitalize() key_nice = key.replace('_', ' ').capitalize()
emoji = emojis.get(key_nice, "") emoji = emojis.get(key_nice, "")
if gfm_supported: if 'Estimated effort to review' in key_nice:
if 'Estimated effort to review' in key_nice: key_nice = 'Estimated effort to review'
key_nice = 'Estimated&nbsp;effort&nbsp;to&nbsp;review [1-5]' value = str(value).strip()
if 'security concerns' in key_nice.lower(): if value.isnumeric():
value = emphasize_header(value.strip()) value_int = int(value)
markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n" else:
elif 'can be split' in key_nice.lower(): try:
markdown_text += process_can_be_split(emoji, value) value_int = int(value.split(',')[0])
elif 'possible issues' in key_nice.lower(): except ValueError:
value = value.strip() continue
issues = value.split('\n- ') blue_bars = '🔵' * value_int
for i, _ in enumerate(issues): white_bars = '' * (5 - value_int)
issues[i] = issues[i].strip().strip('-').strip() value = f"{value_int} {blue_bars}{white_bars}"
issues = unique_strings(issues) # remove duplicates if gfm_supported:
number_of_issues = len(issues) markdown_text += f"<tr><td>"
if number_of_issues > 1: markdown_text += f"{emoji}&nbsp;<strong>{key_nice}</strong>: {value}"
markdown_text += f"<tr><td rowspan={number_of_issues}> {emoji}&nbsp;<strong>{key_nice}</strong></td>\n" markdown_text += f"</td></tr>\n"
for i, issue in enumerate(issues): else:
if not issue: markdown_text += f"### {emoji} {key_nice}: {value}\n\n"
continue elif 'relevant tests' in key_nice.lower():
issue = emphasize_header(issue) value = str(value).strip().lower()
if i == 0: if gfm_supported:
markdown_text += f"<td>\n\n{issue}</td></tr>\n" markdown_text += f"<tr><td>"
else: if is_value_no(value):
markdown_text += f"<tr>\n<td>\n\n{issue}</td></tr>\n" markdown_text += f"{emoji}&nbsp;<strong>No relevant tests</strong>"
else: else:
value = emphasize_header(value.strip('-').strip()) markdown_text += f"{emoji}&nbsp;<strong>PR contains tests</strong>"
markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n" markdown_text += f"</td></tr>\n"
else: else:
markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n" if is_value_no(value):
markdown_text += f'### {emoji} No relevant tests\n\n'
else:
markdown_text += f"### PR contains tests\n\n"
elif 'security concerns' in key_nice.lower():
if gfm_supported:
markdown_text += f"<tr><td>"
if is_value_no(value):
markdown_text += f"{emoji}&nbsp;<strong>No security concerns identified</strong>"
else:
markdown_text += f"{emoji}&nbsp;<strong>Security concerns</strong><br><br>\n\n"
value = emphasize_header(value.strip())
markdown_text += f"{value}"
markdown_text += f"</td></tr>\n"
else:
if is_value_no(value):
markdown_text += f'### {emoji} No security concerns identified\n\n'
else:
markdown_text += f"### {emoji} Security concerns\n\n"
value = emphasize_header(value.strip(), only_markdown=True)
markdown_text += f"{value}\n\n"
elif 'can be split' in key_nice.lower():
if gfm_supported:
markdown_text += f"<tr><td>"
markdown_text += process_can_be_split(emoji, value)
markdown_text += f"</td></tr>\n"
elif 'key issues to review' in key_nice.lower():
# value is a list of issues
if is_value_no(value):
if gfm_supported:
markdown_text += f"<tr><td>"
markdown_text += f"{emoji}&nbsp;<strong>No major issues detected</strong>"
markdown_text += f"</td></tr>\n"
else:
markdown_text += f"### {emoji} No major issues detected\n\n"
else:
issues = value
if gfm_supported:
markdown_text += f"<tr><td>"
# markdown_text += f"{emoji}&nbsp;<strong>{key_nice}</strong><br><br>\n\n"
markdown_text += f"{emoji}&nbsp;<strong>Recommended focus areas for review</strong><br><br>\n\n"
else:
markdown_text += f"### {emoji} Recommended focus areas for review\n\n#### \n"
for i, issue in enumerate(issues):
try:
if not issue or not isinstance(issue, dict):
continue
relevant_file = issue.get('relevant_file', '').strip()
issue_header = issue.get('issue_header', '').strip()
issue_content = issue.get('issue_content', '').strip()
start_line = int(str(issue.get('start_line', 0)).strip())
end_line = int(str(issue.get('end_line', 0)).strip())
reference_link = git_provider.get_line_link(relevant_file, start_line, end_line)
if gfm_supported:
issue_str = f"<a href='{reference_link}'><strong>{issue_header}</strong></a><br>{issue_content}"
else:
issue_str = f"[**{issue_header}**]({reference_link})\n\n{issue_content}\n\n"
markdown_text += f"{issue_str}\n\n"
except Exception as e:
get_logger().exception(f"Failed to process 'Recommended focus areas for review': {e}")
if gfm_supported:
markdown_text += f"</td></tr>\n"
else: else:
if len(value.split()) > 1: if gfm_supported:
markdown_text += f"{emoji} **{key_nice}:**\n\n {value}\n\n" markdown_text += f"<tr><td>"
markdown_text += f"{emoji}&nbsp;<strong>{key_nice}</strong>: {value}"
markdown_text += f"</td></tr>\n"
else: else:
markdown_text += f"{emoji} **{key_nice}:** {value}\n\n" markdown_text += f"### {emoji} {key_nice}: {value}\n\n"
if gfm_supported: if gfm_supported:
markdown_text += "</table>\n" markdown_text += "</table>\n"
@ -144,50 +241,70 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool = True, increment
markdown_text += f"<details><summary> <strong>Code feedback:</strong></summary>\n\n" markdown_text += f"<details><summary> <strong>Code feedback:</strong></summary>\n\n"
markdown_text += "<hr>" markdown_text += "<hr>"
else: else:
markdown_text += f"\n\n** Code feedback:**\n\n" markdown_text += f"\n\n### Code feedback:\n\n"
for i, value in enumerate(output_data['code_feedback']): for i, value in enumerate(output_data['code_feedback']):
if value is None or value == '' or value == {} or value == []: if value is None or value == '' or value == {} or value == []:
continue continue
markdown_text += parse_code_suggestion(value, i, gfm_supported)+"\n\n" markdown_text += parse_code_suggestion(value, i, gfm_supported)+"\n\n"
if markdown_text.endswith('<hr>'): if markdown_text.endswith('<hr>'):
markdown_text = markdown_text[:-4] markdown_text= markdown_text[:-4]
if gfm_supported: if gfm_supported:
markdown_text += f"</details>" markdown_text += f"</details>"
#print(markdown_text)
return markdown_text return markdown_text
def process_can_be_split(emoji, value): def process_can_be_split(emoji, value):
# key_nice = "Can this PR be split?" try:
key_nice = "Multiple PR themes" # key_nice = "Can this PR be split?"
markdown_text = "" key_nice = "Multiple PR themes"
if not value or isinstance(value, list) and len(value) == 1: markdown_text = ""
value = "No" if not value or isinstance(value, list) and len(value) == 1:
markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n" value = "No"
else: # markdown_text += f"<tr><td> {emoji}&nbsp;<strong>{key_nice}</strong></td><td>\n\n{value}\n\n</td></tr>\n"
number_of_splits = len(value) # markdown_text += f"### {emoji} No multiple PR themes\n\n"
markdown_text += f"<tr><td rowspan={number_of_splits}> {emoji}&nbsp;<strong>{key_nice}</strong></td>\n" markdown_text += f"{emoji} <strong>No multiple PR themes</strong>\n\n"
for i, split in enumerate(value): else:
title = split.get('title', '') markdown_text += f"{emoji} <strong>{key_nice}</strong><br><br>\n\n"
relevant_files = split.get('relevant_files', []) for i, split in enumerate(value):
if i == 0: title = split.get('title', '')
markdown_text += f"<td><details><summary>\nSub-PR theme: <strong>{title}</strong></summary>\n\n" relevant_files = split.get('relevant_files', [])
markdown_text += f"<hr>\n" markdown_text += f"<details><summary>\nSub-PR theme: <b>{title}</b></summary>\n\n"
markdown_text += f"Relevant files:\n" markdown_text += f"___\n\nRelevant files:\n\n"
markdown_text += f"<ul>\n"
for file in relevant_files: for file in relevant_files:
markdown_text += f"<li>{file}</li>\n" markdown_text += f"- {file}\n"
markdown_text += f"</ul>\n\n</details></td></tr>\n" markdown_text += f"___\n\n"
else: markdown_text += f"</details>\n\n"
markdown_text += f"<tr>\n<td><details><summary>\nSub-PR theme: <strong>{title}</strong></summary>\n\n"
markdown_text += f"<hr>\n" # markdown_text += f"#### Sub-PR theme: {title}\n\n"
markdown_text += f"Relevant files:\n" # markdown_text += f"Relevant files:\n\n"
markdown_text += f"<ul>\n" # for file in relevant_files:
for file in relevant_files: # markdown_text += f"- {file}\n"
markdown_text += f"<li>{file}</li>\n" # markdown_text += "\n"
markdown_text += f"</ul>\n\n</details></td></tr>\n" # number_of_splits = len(value)
# markdown_text += f"<tr><td rowspan={number_of_splits}> {emoji}&nbsp;<strong>{key_nice}</strong></td>\n"
# for i, split in enumerate(value):
# title = split.get('title', '')
# relevant_files = split.get('relevant_files', [])
# if i == 0:
# markdown_text += f"<td><details><summary>\nSub-PR theme:<br><strong>{title}</strong></summary>\n\n"
# markdown_text += f"<hr>\n"
# markdown_text += f"Relevant files:\n"
# markdown_text += f"<ul>\n"
# for file in relevant_files:
# markdown_text += f"<li>{file}</li>\n"
# markdown_text += f"</ul>\n\n</details></td></tr>\n"
# else:
# markdown_text += f"<tr>\n<td><details><summary>\nSub-PR theme:<br><strong>{title}</strong></summary>\n\n"
# markdown_text += f"<hr>\n"
# markdown_text += f"Relevant files:\n"
# markdown_text += f"<ul>\n"
# for file in relevant_files:
# markdown_text += f"<li>{file}</li>\n"
# markdown_text += f"</ul>\n\n</details></td></tr>\n"
except Exception as e:
get_logger().exception(f"Failed to process can be split: {e}")
return ""
return markdown_text return markdown_text
@ -407,7 +524,7 @@ def update_settings_from_args(args: List[str]) -> List[str]:
arg = arg.strip('-').strip() arg = arg.strip('-').strip()
vals = arg.split('=', 1) vals = arg.split('=', 1)
if len(vals) != 2: if len(vals) != 2:
if len(vals) > 2: # --extended is a valid argument if len(vals) > 2: # --extended is a valid argument
get_logger().error(f'Invalid argument format: {arg}') get_logger().error(f'Invalid argument format: {arg}')
other_args.append(arg) other_args.append(arg)
continue continue
@ -429,28 +546,37 @@ def _fix_key_value(key: str, value: str):
return key, value return key, value
def load_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict: def load_yaml(response_text: str, keys_fix_yaml: List[str] = [], first_key="", last_key="") -> dict:
response_text = response_text.removeprefix('```yaml').rstrip('`') response_text = response_text.strip('\n').removeprefix('```yaml').rstrip().removesuffix('```')
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().warning(f"Initial failure to parse AI prediction: {e}")
data = try_fix_yaml(response_text, keys_fix_yaml=keys_fix_yaml) data = try_fix_yaml(response_text, keys_fix_yaml=keys_fix_yaml, first_key=first_key, last_key=last_key)
if not data:
get_logger().error(f"Failed to parse AI prediction after fallbacks", artifact={'response_text': response_text})
else:
get_logger().info(f"Successfully parsed AI prediction after fallbacks",
artifact={'response_text': response_text})
return data return data
def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict:
def try_fix_yaml(response_text: str,
keys_fix_yaml: List[str] = [],
first_key="",
last_key="",) -> dict:
response_text_lines = response_text.split('\n') response_text_lines = response_text.split('\n')
keys = ['relevant line:', 'suggestion content:', 'relevant file:', 'existing code:', 'improved code:'] keys_yaml = ['relevant line:', 'suggestion content:', 'relevant file:', 'existing code:', 'improved code:']
keys = keys + keys_fix_yaml keys_yaml = keys_yaml + 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)):
for key in keys: for key in keys_yaml:
if key in response_text_lines_copy[i] and not '|-' in response_text_lines_copy[i]: if key in response_text_lines_copy[i] and not '|' in response_text_lines_copy[i]:
response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}', response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}',
f'{key} |-\n ') f'{key} |\n ')
try: try:
data = yaml.safe_load('\n'.join(response_text_lines_copy)) data = yaml.safe_load('\n'.join(response_text_lines_copy))
get_logger().info(f"Successfully parsed AI prediction after adding |-\n") get_logger().info(f"Successfully parsed AI prediction after adding |-\n")
@ -470,7 +596,8 @@ def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict:
except: except:
pass pass
# third 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('}').rstrip(':\n') 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)
@ -479,7 +606,28 @@ def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict:
except: except:
pass pass
# fourth fallback - try to remove last lines
# forth fallback - try to extract yaml snippet by 'first_key' and 'last_key'
# note that 'last_key' can be in practice a key that is not the last key in the yaml snippet.
# it just needs to be some inner key, so we can look for newlines after it
if first_key and last_key:
index_start = response_text.find(f"\n{first_key}:")
if index_start == -1:
index_start = response_text.find(f"{first_key}:")
index_last_code = response_text.rfind(f"{last_key}:")
index_end = response_text.find("\n\n", index_last_code) # look for newlines after last_key
if index_end == -1:
index_end = len(response_text)
response_text_copy = response_text[index_start:index_end].strip().strip('```yaml').strip('`').strip()
try:
data = yaml.safe_load(response_text_copy)
get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet")
return data
except:
pass
# fifth fallback - try to remove last lines
data = {} data = {}
for i in range(1, len(response_text_lines)): for i in range(1, len(response_text_lines)):
response_text_lines_tmp = '\n'.join(response_text_lines[:-i]) response_text_lines_tmp = '\n'.join(response_text_lines[:-i])
@ -495,7 +643,7 @@ def set_custom_labels(variables, git_provider=None):
if not get_settings().config.enable_custom_labels: if not get_settings().config.enable_custom_labels:
return return
labels = get_settings().custom_labels labels = get_settings().get('custom_labels', {})
if not labels: if not labels:
# set default labels # set default labels
labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other'] labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
@ -521,14 +669,16 @@ def get_user_labels(current_labels: List[str] = None):
Only keep labels that has been added by the user Only keep labels that has been added by the user
""" """
try: try:
enable_custom_labels = get_settings().config.get('enable_custom_labels', False)
custom_labels = get_settings().get('custom_labels', [])
if current_labels is None: if current_labels is None:
current_labels = [] current_labels = []
user_labels = [] user_labels = []
for label in current_labels: for label in current_labels:
if label.lower() in ['bug fix', 'tests', 'enhancement', 'documentation', 'other']: if label.lower() in ['bug fix', 'tests', 'enhancement', 'documentation', 'other']:
continue continue
if get_settings().config.enable_custom_labels: if enable_custom_labels:
if label in get_settings().custom_labels: if label in custom_labels:
continue continue
user_labels.append(label) user_labels.append(label)
if user_labels: if user_labels:
@ -540,19 +690,29 @@ def get_user_labels(current_labels: List[str] = None):
def get_max_tokens(model): def get_max_tokens(model):
"""
Get the maximum number of tokens allowed for a model.
logic:
(1) If the model is in './pr_agent/algo/__init__.py', use the value from there.
(2) else, the user needs to define explicitly 'config.custom_model_max_tokens'
For both cases, we further limit the number of tokens to 'config.max_model_tokens' if it is set.
This aims to improve the algorithmic quality, as the AI model degrades in performance when the input is too long.
"""
settings = get_settings() settings = get_settings()
if model in MAX_TOKENS: if model in MAX_TOKENS:
max_tokens_model = MAX_TOKENS[model] max_tokens_model = MAX_TOKENS[model]
elif settings.config.custom_model_max_tokens > 0:
max_tokens_model = settings.config.custom_model_max_tokens
else: else:
raise Exception(f"MAX_TOKENS must be set for model {model} in ./pr_agent/algo/__init__.py") raise Exception(f"Ensure {model} is defined in MAX_TOKENS in ./pr_agent/algo/__init__.py or set a positive value for it in config.custom_model_max_tokens")
if settings.config.max_model_tokens: if settings.config.max_model_tokens and settings.config.max_model_tokens > 0:
max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model) max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model)
# get_logger().debug(f"limiting max tokens to {max_tokens_model}")
return max_tokens_model return max_tokens_model
def clip_tokens(text: str, max_tokens: int, add_three_dots=True) -> str: def clip_tokens(text: str, max_tokens: int, add_three_dots=True, num_input_tokens=None, delete_last_line=False) -> str:
""" """
Clip the number of tokens in a string to a maximum number of tokens. Clip the number of tokens in a string to a maximum number of tokens.
@ -567,16 +727,30 @@ def clip_tokens(text: str, max_tokens: int, add_three_dots=True) -> str:
return text return text
try: try:
encoder = TokenEncoder.get_token_encoder() if num_input_tokens is None:
num_input_tokens = len(encoder.encode(text)) encoder = TokenEncoder.get_token_encoder()
num_input_tokens = len(encoder.encode(text))
if num_input_tokens <= max_tokens: if num_input_tokens <= max_tokens:
return text return text
if max_tokens < 0:
return ""
# calculate the number of characters to keep
num_chars = len(text) num_chars = len(text)
chars_per_token = num_chars / num_input_tokens chars_per_token = num_chars / num_input_tokens
num_output_chars = int(chars_per_token * max_tokens) factor = 0.9 # reduce by 10% to be safe
clipped_text = text[:num_output_chars] num_output_chars = int(factor * chars_per_token * max_tokens)
if add_three_dots:
clipped_text += "\n...(truncated)" # clip the text
if num_output_chars > 0:
clipped_text = text[:num_output_chars]
if delete_last_line:
clipped_text = clipped_text.rsplit('\n', 1)[0]
if add_three_dots:
clipped_text += "\n...(truncated)"
else: # if the text is empty
clipped_text = ""
return clipped_text return clipped_text
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}")
@ -586,6 +760,7 @@ def replace_code_tags(text):
""" """
Replace odd instances of ` with <code> and even instances of ` with </code> Replace odd instances of ` with <code> and even instances of ` with </code>
""" """
text = html.escape(text)
parts = text.split('`') parts = text.split('`')
for i in range(1, len(parts), 2): for i in range(1, len(parts), 2):
parts[i] = '<code>' + parts[i] + '</code>' parts[i] = '<code>' + parts[i] + '</code>'
@ -602,6 +777,9 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
re_hunk_header = re.compile( re_hunk_header = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
if not diff_files:
return position, absolute_position
for file in diff_files: for file in diff_files:
if file.filename and (file.filename.strip() == relevant_file): if file.filename and (file.filename.strip() == relevant_file):
patch = file.patch patch = file.patch
@ -663,11 +841,62 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
break break
return position, absolute_position return position, absolute_position
def validate_and_await_rate_limit(rate_limit_status=None, git_provider=None, get_rate_limit_status_func=None):
if git_provider and not rate_limit_status:
rate_limit_status = {'resources': git_provider.github_client.get_rate_limit().raw_data}
if not rate_limit_status:
rate_limit_status = get_rate_limit_status_func()
# validate that the rate limit is not exceeded
is_rate_limit = False
for key, value in rate_limit_status['resources'].items():
if value['remaining'] == 0:
print(f"key: {key}, value: {value}")
is_rate_limit = True
sleep_time_sec = value['reset'] - datetime.now().timestamp()
sleep_time_hour = sleep_time_sec / 3600.0
print(f"Rate limit exceeded. Sleeping for {sleep_time_hour} hours")
if sleep_time_sec > 0:
time.sleep(sleep_time_sec+1)
if git_provider:
rate_limit_status = {'resources': git_provider.github_client.get_rate_limit().raw_data}
else:
rate_limit_status = get_rate_limit_status_func()
return is_rate_limit
def get_largest_component(pr_url):
from pr_agent.tools.pr_analyzer import PRAnalyzer
publish_output = get_settings().config.publish_output
get_settings().config.publish_output = False # disable publish output
analyzer = PRAnalyzer(pr_url)
methods_dict_files = analyzer.run_sync()
get_settings().config.publish_output = publish_output
max_lines_changed = 0
file_b = ""
component_name_b = ""
for file in methods_dict_files:
for method in methods_dict_files[file]:
try:
if methods_dict_files[file][method]['num_plus_lines'] > max_lines_changed:
max_lines_changed = methods_dict_files[file][method]['num_plus_lines']
file_b = file
component_name_b = method
except:
pass
if component_name_b:
get_logger().info(f"Using the largest changed component: '{component_name_b}'")
return component_name_b, file_b
else:
return None, None
def github_action_output(output_data: dict, key_name: str): def github_action_output(output_data: dict, key_name: str):
try: try:
if not get_settings().get('github_action_config.enable_output', False): if not get_settings().get('github_action_config.enable_output', False):
return return
key_data = output_data.get(key_name, {}) key_data = output_data.get(key_name, {})
with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
print(f"{key_name}={json.dumps(key_data, indent=None, ensure_ascii=False)}", file=fh) print(f"{key_name}={json.dumps(key_data, indent=None, ensure_ascii=False)}", file=fh)
@ -677,23 +906,104 @@ def github_action_output(output_data: dict, key_name: str):
def show_relevant_configurations(relevant_section: str) -> str: def show_relevant_configurations(relevant_section: str) -> str:
forbidden_keys = ['ai_disclaimer', 'ai_disclaimer_title', 'ANALYTICS_FOLDER', 'secret_provider', skip_keys = ['ai_disclaimer', 'ai_disclaimer_title', 'ANALYTICS_FOLDER', 'secret_provider', "skip_keys",
'trial_prefix_message', 'no_eligible_message', 'identity_provider', 'ALLOWED_REPOS','APP_NAME'] 'trial_prefix_message', 'no_eligible_message', 'identity_provider', 'ALLOWED_REPOS','APP_NAME']
extra_skip_keys = get_settings().config.get('config.skip_keys', [])
if extra_skip_keys:
skip_keys.extend(extra_skip_keys)
markdown_text = "" markdown_text = ""
markdown_text += "\n<hr>\n<details> <summary><strong>🛠️ Relevant configurations:</strong></summary> \n\n" markdown_text += "\n<hr>\n<details> <summary><strong>🛠️ Relevant configurations:</strong></summary> \n\n"
markdown_text +="<br>These are the relevant [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) for this tool:\n\n" markdown_text +="<br>These are the relevant [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) for this tool:\n\n"
markdown_text += f"**[config**]\n```yaml\n\n" markdown_text += f"**[config**]\n```yaml\n\n"
for key, value in get_settings().config.items(): for key, value in get_settings().config.items():
if key in forbidden_keys: if key in skip_keys:
continue continue
markdown_text += f"{key}: {value}\n" markdown_text += f"{key}: {value}\n"
markdown_text += "\n```\n" markdown_text += "\n```\n"
markdown_text += f"\n**[{relevant_section}]**\n```yaml\n\n" markdown_text += f"\n**[{relevant_section}]**\n```yaml\n\n"
for key, value in get_settings().get(relevant_section, {}).items(): for key, value in get_settings().get(relevant_section, {}).items():
if key in forbidden_keys: if key in skip_keys:
continue continue
markdown_text += f"{key}: {value}\n" markdown_text += f"{key}: {value}\n"
markdown_text += "\n```" markdown_text += "\n```"
markdown_text += "\n</details>\n" markdown_text += "\n</details>\n"
return markdown_text return markdown_text
def is_value_no(value):
if not value:
return True
value_str = str(value).strip().lower()
if value_str == 'no' or value_str == 'none' or value_str == 'false':
return True
return False
def process_description(description_full: str) -> Tuple[str, List]:
if not description_full:
return "", []
split_str = "### **Changes walkthrough** 📝"
description_split = description_full.split(split_str)
base_description_str = description_split[0]
changes_walkthrough_str = ""
files = []
if len(description_split) > 1:
changes_walkthrough_str = description_split[1]
else:
get_logger().debug("No changes walkthrough found")
try:
if changes_walkthrough_str:
# get the end of the table
if '</table>\n\n___' in changes_walkthrough_str:
end = changes_walkthrough_str.index("</table>\n\n___")
elif '\n___' in changes_walkthrough_str:
end = changes_walkthrough_str.index("\n___")
else:
end = len(changes_walkthrough_str)
changes_walkthrough_str = changes_walkthrough_str[:end]
h = html2text.HTML2Text()
h.body_width = 0 # Disable line wrapping
# find all the files
pattern = r'<tr>\s*<td>\s*(<details>\s*<summary>(.*?)</summary>(.*?)</details>)\s*</td>'
files_found = re.findall(pattern, changes_walkthrough_str, re.DOTALL)
for file_data in files_found:
try:
if isinstance(file_data, tuple):
file_data = file_data[0]
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*<li>(.*?)</details>'
res = re.search(pattern, file_data, re.DOTALL)
if not res or res.lastindex != 4:
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong><dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\n\n\s*(.*?)</details>'
res = re.search(pattern_back, file_data, re.DOTALL)
if res and res.lastindex == 4:
short_filename = res.group(1).strip()
short_summary = res.group(2).strip()
long_filename = res.group(3).strip()
long_summary = res.group(4).strip()
long_summary = long_summary.replace('<br> *', '\n*').replace('<br>','').replace('\n','<br>')
long_summary = h.handle(long_summary).strip()
if long_summary.startswith('\\-'):
long_summary = "* " + long_summary[2:]
elif not long_summary.startswith('*'):
long_summary = f"* {long_summary}"
files.append({
'short_file_name': short_filename,
'full_file_name': long_filename,
'short_summary': short_summary,
'long_summary': long_summary
})
else:
get_logger().error(f"Failed to parse description", artifact={'description': file_data})
except Exception as e:
get_logger().exception(f"Failed to process description: {e}", artifact={'description': file_data})
except Exception as e:
get_logger().exception(f"Failed to process description: {e}")
return base_description_str, files

View File

@ -4,11 +4,12 @@ import os
from pr_agent.agent.pr_agent import PRAgent, commands from pr_agent.agent.pr_agent import PRAgent, commands
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.log import setup_logger from pr_agent.log import setup_logger, get_logger
log_level = os.environ.get("LOG_LEVEL", "INFO") log_level = os.environ.get("LOG_LEVEL", "INFO")
setup_logger(log_level) setup_logger(log_level)
def set_parser(): def set_parser():
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage= parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage=
"""\ """\
@ -50,6 +51,7 @@ def set_parser():
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[]) parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
return parser return parser
def run_command(pr_url, command): def run_command(pr_url, command):
# Preparing the command # Preparing the command
run_command_str = f"--pr_url={pr_url} {command.lstrip('/')}" run_command_str = f"--pr_url={pr_url} {command.lstrip('/')}"
@ -58,6 +60,7 @@ def run_command(pr_url, command):
# Run the command. Feedback will appear in GitHub PR comments # Run the command. Feedback will appear in GitHub PR comments
run(args=args) run(args=args)
def run(inargs=None, args=None): def run(inargs=None, args=None):
parser = set_parser() parser = set_parser()
if not args: if not args:
@ -68,10 +71,21 @@ def run(inargs=None, args=None):
command = args.command.lower() command = args.command.lower()
get_settings().set("CONFIG.CLI_MODE", True) get_settings().set("CONFIG.CLI_MODE", True)
if args.issue_url:
result = asyncio.run(PRAgent().handle_request(args.issue_url, [command] + args.rest)) async def inner():
else: if args.issue_url:
result = asyncio.run(PRAgent().handle_request(args.pr_url, [command] + args.rest)) result = await asyncio.create_task(PRAgent().handle_request(args.issue_url, [command] + args.rest))
else:
result = await asyncio.create_task(PRAgent().handle_request(args.pr_url, [command] + args.rest))
if get_settings().litellm.get("enable_callbacks", False):
# There may be additional events on the event queue from the run above. If there are give them time to complete.
get_logger().debug("Waiting for event queue to complete")
await asyncio.wait([task for task in asyncio.all_tasks() if task is not asyncio.current_task()])
return result
result = asyncio.run(inner())
if not result: if not result:
parser.print_help() parser.print_help()

View File

@ -27,13 +27,23 @@ global_settings = Dynaconf(
"settings/pr_update_changelog_prompts.toml", "settings/pr_update_changelog_prompts.toml",
"settings/pr_custom_labels.toml", "settings/pr_custom_labels.toml",
"settings/pr_add_docs.toml", "settings/pr_add_docs.toml",
"settings/custom_labels.toml",
"settings/pr_help_prompts.toml",
"settings_prod/.secrets.toml", "settings_prod/.secrets.toml",
"settings/custom_labels.toml"
]] ]]
) )
def get_settings(): def get_settings():
"""
Retrieves the current settings.
This function attempts to fetch the settings from the starlette_context's context object. If it fails,
it defaults to the global settings defined outside of this function.
Returns:
Dynaconf: The current settings object, either from the context or the global default.
"""
try: try:
return context["settings"] return context["settings"]
except Exception: except Exception:
@ -41,7 +51,7 @@ def get_settings():
# Add local configuration from pyproject.toml of the project being reviewed # Add local configuration from pyproject.toml of the project being reviewed
def _find_repository_root() -> Path: def _find_repository_root() -> Optional[Path]:
""" """
Identify project root directory by recursively searching for the .git directory in the parent directories. Identify project root directory by recursively searching for the .git directory in the parent directories.
""" """
@ -61,7 +71,7 @@ def _find_pyproject() -> Optional[Path]:
""" """
repo_root = _find_repository_root() repo_root = _find_repository_root()
if repo_root: if repo_root:
pyproject = _find_repository_root() / "pyproject.toml" pyproject = repo_root / "pyproject.toml"
return pyproject if pyproject.is_file() else None return pyproject if pyproject.is_file() else None
return None return None

View File

@ -2,12 +2,13 @@ from pr_agent.config_loader import get_settings
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
from pr_agent.git_providers.git_provider import GitProvider
from pr_agent.git_providers.github_provider import GithubProvider from pr_agent.git_providers.github_provider import GithubProvider
from pr_agent.git_providers.gitlab_provider import GitLabProvider from pr_agent.git_providers.gitlab_provider import GitLabProvider
from pr_agent.git_providers.local_git_provider import LocalGitProvider from pr_agent.git_providers.local_git_provider import LocalGitProvider
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
from pr_agent.git_providers.gerrit_provider import GerritProvider from pr_agent.git_providers.gerrit_provider import GerritProvider
from starlette_context import context
_GIT_PROVIDERS = { _GIT_PROVIDERS = {
'github': GithubProvider, 'github': GithubProvider,
@ -16,10 +17,11 @@ _GIT_PROVIDERS = {
'bitbucket_server': BitbucketServerProvider, 'bitbucket_server': BitbucketServerProvider,
'azure': AzureDevopsProvider, 'azure': AzureDevopsProvider,
'codecommit': CodeCommitProvider, 'codecommit': CodeCommitProvider,
'local' : LocalGitProvider, 'local': LocalGitProvider,
'gerrit': GerritProvider, 'gerrit': GerritProvider,
} }
def get_git_provider(): def get_git_provider():
try: try:
provider_id = get_settings().config.git_provider provider_id = get_settings().config.git_provider
@ -28,3 +30,33 @@ def get_git_provider():
if provider_id not in _GIT_PROVIDERS: if provider_id not in _GIT_PROVIDERS:
raise ValueError(f"Unknown git provider: {provider_id}") raise ValueError(f"Unknown git provider: {provider_id}")
return _GIT_PROVIDERS[provider_id] return _GIT_PROVIDERS[provider_id]
def get_git_provider_with_context(pr_url) -> GitProvider:
"""
Get a GitProvider instance for the given PR URL. If the GitProvider instance is already in the context, return it.
"""
is_context_env = None
try:
is_context_env = context.get("settings", None)
except Exception:
pass # we are not in a context environment (CLI)
# check if context["git_provider"]["pr_url"] exists
if is_context_env and context.get("git_provider", {}).get("pr_url", {}):
git_provider = context["git_provider"]["pr_url"]
# possibly check if the git_provider is still valid, or if some reset is needed
# ...
return git_provider
else:
try:
provider_id = get_settings().config.git_provider
if provider_id not in _GIT_PROVIDERS:
raise ValueError(f"Unknown git provider: {provider_id}")
git_provider = _GIT_PROVIDERS[provider_id](pr_url)
if is_context_env:
context["git_provider"] = {pr_url: git_provider}
return git_provider
except Exception as e:
raise ValueError(f"Failed to get git provider for {pr_url}") from e

View File

@ -2,6 +2,7 @@ import os
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from ..algo.file_filter import filter_ignored
from ..log import get_logger from ..log import get_logger
from ..algo.language_handler import is_valid_file 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 ..algo.utils import clip_tokens, find_line_number_of_relevant_line_in_file, load_large_diff
@ -164,7 +165,7 @@ class AzureDevopsProvider(GitProvider):
pull_request_id=self.pr_num, pull_request_id=self.pr_num,
) )
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}") get_logger().warning(f"Failed to publish labels, error: {e}")
def get_pr_labels(self, update=False): def get_pr_labels(self, update=False):
try: try:
@ -284,8 +285,20 @@ class AzureDevopsProvider(GitProvider):
# #
# diffs = list(set(diffs)) # diffs = list(set(diffs))
diffs_original = diffs
diffs = filter_ignored(diffs_original, 'azure')
if diffs_original != diffs:
try:
get_logger().info(f"Filtered out [ignore] files for pull request:", extra=
{"files": diffs_original, # diffs is just a list of names
"filtered_files": diffs})
except Exception:
pass
invalid_files_names = []
for file in diffs: for file in diffs:
if not is_valid_file(file): if not is_valid_file(file):
invalid_files_names.append(file)
continue continue
version = GitVersionDescriptor( version = GitVersionDescriptor(
@ -303,7 +316,7 @@ class AzureDevopsProvider(GitProvider):
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(f"Failed to retrieve new file content of {file} at version {version}. Error: {str(error)}") get_logger().error(f"Failed to retrieve new file content of {file} at version {version}", error=error)
# get_logger().error( # get_logger().error(
# "Failed to retrieve new file content of %s at version %s. Error: %s", # "Failed to retrieve new file content of %s at version %s. Error: %s",
# file, # file,
@ -334,13 +347,18 @@ class AzureDevopsProvider(GitProvider):
) )
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(f"Failed to retrieve original file content of {file} at version {version}. Error: {str(error)}") get_logger().error(f"Failed to retrieve original file content of {file} at version {version}", error=error)
original_file_content_str = "" original_file_content_str = ""
patch = load_large_diff( patch = load_large_diff(
file, new_file_content_str, original_file_content_str, show_warning=False file, new_file_content_str, original_file_content_str, show_warning=False
).rstrip() ).rstrip()
# count number of lines added and removed
patch_lines = patch.splitlines(keepends=True)
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
diff_files.append( diff_files.append(
FilePatchInfo( FilePatchInfo(
original_file_content_str, original_file_content_str,
@ -348,17 +366,21 @@ class AzureDevopsProvider(GitProvider):
patch=patch, patch=patch,
filename=file, filename=file,
edit_type=edit_type, edit_type=edit_type,
num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines,
) )
) )
get_logger().info(f"Invalid files: {invalid_files_names}")
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)}") get_logger().exception(f"Failed to get diff files, error: {e}")
return [] return []
def publish_comment(self, pr_comment: str, is_temporary: bool = False, thread_context=None): 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_context=thread_context, status=1) thread = CommentThread(comments=[comment], thread_context=thread_context, status=5)
thread_response = self.azure_devops_client.create_thread( thread_response = self.azure_devops_client.create_thread(
comment_thread=thread, comment_thread=thread,
project=self.workspace_slug, project=self.workspace_slug,
@ -410,7 +432,7 @@ class AzureDevopsProvider(GitProvider):
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to remove temp comments, error: {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, original_suggestion=None):
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)])
@ -430,7 +452,7 @@ class AzureDevopsProvider(GitProvider):
return dict(body=body, path=path, position=position, absolute_position=absolute_position) if subject_type == "LINE" else {} 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): def publish_inline_comments(self, comments: list[dict], disable_fallback: bool = False):
overall_sucess = True overall_success = True
for comment in comments: for comment in comments:
try: try:
self.publish_comment(comment["body"], self.publish_comment(comment["body"],
@ -452,8 +474,8 @@ class AzureDevopsProvider(GitProvider):
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}")
overall_sucess = False overall_success = False
return overall_sucess return overall_success
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
@ -494,19 +516,20 @@ class AzureDevopsProvider(GitProvider):
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, *, full: bool = True) -> str:
max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
if max_tokens:
return clip_tokens(self.pr.description, max_tokens)
return self.pr.description
def get_user_id(self): def get_user_id(self):
return 0 return 0
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError( threads = self.azure_devops_client.get_threads(repository_id=self.repo_slug, pull_request_id=self.pr_num, project=self.workspace_slug)
"Azure DevOps provider does not support issue comments yet" threads.reverse()
) comment_list = []
for thread in threads:
for comment in thread.comments:
if comment.content and comment not in comment_list:
comment.body = comment.content
comment.thread_id = thread.id
comment_list.append(comment)
return comment_list
def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
return True return True
@ -519,18 +542,20 @@ class AzureDevopsProvider(GitProvider):
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 "pullrequest" not in path_parts:
if len(path_parts) < 6 or path_parts[4] != "pullrequest":
raise ValueError( raise ValueError(
"The provided URL does not appear to be a Azure DevOps PR URL" "The provided URL does not appear to be a Azure DevOps PR URL"
) )
if len(path_parts) == 6: # "https://dev.azure.com/organization/project/_git/repo/pullrequest/1"
workspace_slug = path_parts[1] workspace_slug = path_parts[1]
repo_slug = path_parts[3] repo_slug = path_parts[3]
try:
pr_number = int(path_parts[5]) pr_number = int(path_parts[5])
except ValueError as e: elif len(path_parts) == 5: # 'https://organization.visualstudio.com/project/_git/repo/pullrequest/1'
raise ValueError("Unable to convert PR number to integer") from e workspace_slug = path_parts[0]
repo_slug = path_parts[2]
pr_number = int(path_parts[4])
else:
raise ValueError("The provided URL does not appear to be a Azure DevOps PR URL")
return workspace_slug, repo_slug, pr_number return workspace_slug, repo_slug, pr_number
@ -590,3 +615,6 @@ class AzureDevopsProvider(GitProvider):
get_logger().error(f"Failed to get pr id, error: {e}") get_logger().error(f"Failed to get pr id, error: {e}")
return "" return ""
def publish_file_comments(self, file_comments: list) -> bool:
pass

View File

@ -7,10 +7,18 @@ from atlassian.bitbucket import Cloud
from starlette_context import context from starlette_context import context
from pr_agent.algo.types import FilePatchInfo, EDIT_TYPE from pr_agent.algo.types import FilePatchInfo, EDIT_TYPE
from ..algo.file_filter import filter_ignored
from ..algo.language_handler import is_valid_file
from ..algo.utils import find_line_number_of_relevant_line_in_file 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 GitProvider from .git_provider import GitProvider, MAX_FILES_ALLOWED_FULL
def _gef_filename(diff):
if diff.new.path:
return diff.new.path
return diff.old.path
class BitbucketProvider(GitProvider): class BitbucketProvider(GitProvider):
@ -28,6 +36,7 @@ class BitbucketProvider(GitProvider):
s.headers["Content-Type"] = "application/json" s.headers["Content-Type"] = "application/json"
self.headers = s.headers self.headers = s.headers
self.bitbucket_client = Cloud(session=s) self.bitbucket_client = Cloud(session=s)
self.max_comment_length = 31000
self.workspace_slug = None self.workspace_slug = None
self.repo_slug = None self.repo_slug = None
self.repo = None self.repo = None
@ -37,6 +46,7 @@ class BitbucketProvider(GitProvider):
self.temp_comments = [] self.temp_comments = []
self.incremental = incremental self.incremental = incremental
self.diff_files = None self.diff_files = None
self.git_files = None
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"]["comments"]["href"] self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"]["comments"]["href"]
@ -106,8 +116,12 @@ class BitbucketProvider(GitProvider):
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 publish_file_comments(self, file_comments: list) -> bool:
pass
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'publish_inline_comments', 'get_labels', 'gfm_markdown']: if capability in ['get_issue_comments', 'publish_inline_comments', 'get_labels', 'gfm_markdown',
'publish_file_comments']:
return False return False
return True return True
@ -116,28 +130,132 @@ class BitbucketProvider(GitProvider):
self.pr = self._get_pr() self.pr = self._get_pr()
def get_files(self): def get_files(self):
return [diff.new.path for diff in self.pr.diffstat()] try:
git_files = context.get("git_files", None)
if git_files:
return git_files
self.git_files = [_gef_filename(diff) for diff in self.pr.diffstat()]
context["git_files"] = self.git_files
return self.git_files
except Exception:
if not self.git_files:
self.git_files = [_gef_filename(diff) for diff in self.pr.diffstat()]
return self.git_files
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
if self.diff_files: if self.diff_files:
return self.diff_files return self.diff_files
diffs = self.pr.diffstat() diffs_original = list(self.pr.diffstat())
diff_split = [ diffs = filter_ignored(diffs_original, 'bitbucket')
"diff --git%s" % x for x in self.pr.diff().split("diff --git") if x.strip() if diffs != diffs_original:
] try:
names_original = [d.new.path for d in diffs_original]
names_kept = [d.new.path for d in diffs]
names_filtered = list(set(names_original) - set(names_kept))
get_logger().info(f"Filtered out [ignore] files for PR", extra={
'original_files': names_original,
'names_kept': names_kept,
'names_filtered': names_filtered
})
except Exception as e:
pass
# get the pr patches
try:
pr_patches = self.pr.diff()
except Exception as e:
# Try different encodings if UTF-8 fails
get_logger().warning(f"Failed to decode PR patch with utf-8, error: {e}")
encodings_to_try = ['iso-8859-1', 'latin-1', 'ascii', 'utf-16']
pr_patches = None
for encoding in encodings_to_try:
try:
pr_patches = self.pr.diff(encoding=encoding)
get_logger().info(f"Successfully decoded PR patch with encoding {encoding}")
break
except UnicodeDecodeError:
continue
if pr_patches is None:
raise ValueError(f"Failed to decode PR patch with encodings {encodings_to_try}")
diff_split = ["diff --git" + x for x in pr_patches.split("diff --git") if x.strip()]
# filter all elements of 'diff_split' that are of indices in 'diffs_original' that are not in 'diffs'
if len(diff_split) > len(diffs) and len(diffs_original) == len(diff_split):
diff_split = [diff_split[i] for i in range(len(diff_split)) if diffs_original[i] in diffs]
if len(diff_split) != len(diffs):
get_logger().error(f"Error - failed to split the diff into {len(diffs)} parts")
return []
# bitbucket diff has a header for each file, we need to remove it:
# "diff --git filename
# new file mode 100644 (optional)
# index caa56f0..61528d7 100644
# --- a/pr_agent/cli_pip.py
# +++ b/pr_agent/cli_pip.py
# @@ -... @@"
for i, _ in enumerate(diff_split):
diff_split_lines = diff_split[i].splitlines()
if (len(diff_split_lines) >= 6) and \
((diff_split_lines[2].startswith("---") and
diff_split_lines[3].startswith("+++") and
diff_split_lines[4].startswith("@@")) or
(diff_split_lines[3].startswith("---") and # new or deleted file
diff_split_lines[4].startswith("+++") and
diff_split_lines[5].startswith("@@"))):
diff_split[i] = "\n".join(diff_split_lines[4:])
else:
if diffs[i].data.get('lines_added', 0) == 0 and diffs[i].data.get('lines_removed', 0) == 0:
diff_split[i] = ""
elif len(diff_split_lines) <= 3:
diff_split[i] = ""
get_logger().info(f"Disregarding empty diff for file {_gef_filename(diffs[i])}")
else:
get_logger().warning(f"Bitbucket failed to get diff for file {_gef_filename(diffs[i])}")
diff_split[i] = ""
invalid_files_names = []
diff_files = [] diff_files = []
counter_valid = 0
# get full files
for index, diff in enumerate(diffs): for index, diff in enumerate(diffs):
original_file_content_str = self._get_pr_file_content( file_path = _gef_filename(diff)
diff.old.get_data("links") if not is_valid_file(file_path):
) invalid_files_names.append(file_path)
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links")) continue
try:
counter_valid += 1
if get_settings().get("bitbucket_app.avoid_full_files", False):
original_file_content_str = ""
new_file_content_str = ""
elif counter_valid < MAX_FILES_ALLOWED_FULL // 2: # factor 2 because bitbucket has limited API calls
if diff.old.get_data("links"):
original_file_content_str = self._get_pr_file_content(
diff.old.get_data("links")['self']['href'])
else:
original_file_content_str = ""
if diff.new.get_data("links"):
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links")['self']['href'])
else:
new_file_content_str = ""
else:
if counter_valid == MAX_FILES_ALLOWED_FULL // 2:
get_logger().info(
f"Bitbucket too many files in PR, will avoid loading full content for rest of files")
original_file_content_str = ""
new_file_content_str = ""
except Exception as e:
get_logger().exception(f"Error - bitbucket failed to get file content, error: {e}")
original_file_content_str = ""
new_file_content_str = ""
file_patch_canonic_structure = FilePatchInfo( file_patch_canonic_structure = FilePatchInfo(
original_file_content_str, original_file_content_str,
new_file_content_str, new_file_content_str,
diff_split[index], diff_split[index],
diff.new.path, file_path,
) )
if diff.data['status'] == 'added': if diff.data['status'] == 'added':
@ -150,6 +268,8 @@ class BitbucketProvider(GitProvider):
file_patch_canonic_structure.edit_type = EDIT_TYPE.RENAMED file_patch_canonic_structure.edit_type = EDIT_TYPE.RENAMED
diff_files.append(file_patch_canonic_structure) diff_files.append(file_patch_canonic_structure)
if invalid_files_names:
get_logger().info(f"Disregarding files with invalid extensions:\n{invalid_files_names}")
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
@ -172,7 +292,7 @@ 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### ({name.capitalize()} 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
@ -189,6 +309,7 @@ class BitbucketProvider(GitProvider):
self.publish_comment(pr_comment) self.publish_comment(pr_comment)
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
pr_comment = self.limit_output_characters(pr_comment, self.max_comment_length)
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"])
@ -196,6 +317,7 @@ class BitbucketProvider(GitProvider):
def edit_comment(self, comment, body: str): def edit_comment(self, comment, body: str):
try: try:
body = self.limit_output_characters(body, self.max_comment_length)
comment.update(body) comment.update(body)
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to update comment, error: {e}") get_logger().exception(f"Failed to update comment, error: {e}")
@ -213,11 +335,14 @@ class BitbucketProvider(GitProvider):
except Exception as e: except Exception as e:
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 # function to create_inline_comment
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str,
absolute_position: int = None):
body = self.limit_output_characters(body, self.max_comment_length)
position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(),
relevant_file.strip('`'), relevant_file.strip('`'),
relevant_line_in_file, absolute_position) 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}")
@ -227,9 +352,9 @@ class BitbucketProvider(GitProvider):
path = relevant_file.strip() path = relevant_file.strip()
return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {} return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {}
def publish_inline_comment(self, comment: str, from_line: int, file: str, original_suggestion=None):
def publish_inline_comment(self, comment: str, from_line: int, file: str): comment = self.limit_output_characters(comment, self.max_comment_length)
payload = json.dumps( { payload = json.dumps({
"content": { "content": {
"raw": comment, "raw": comment,
}, },
@ -274,10 +399,10 @@ class BitbucketProvider(GitProvider):
for comment in comments: for comment in comments:
if 'position' in comment: if 'position' in comment:
self.publish_inline_comment(comment['body'], comment['position'], comment['path']) self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
elif 'start_line' in comment: # multi-line comment elif 'start_line' in comment: # multi-line comment
# note that bitbucket does not seem to support range - only a comment on a single line - https://community.developer.atlassian.com/t/api-post-endpoint-for-inline-pull-request-comments/60452 # note that bitbucket does not seem to support range - only a comment on a single line - https://community.developer.atlassian.com/t/api-post-endpoint-for-inline-pull-request-comments/60452
self.publish_inline_comment(comment['body'], comment['start_line'], comment['path']) self.publish_inline_comment(comment['body'], comment['start_line'], comment['path'])
elif 'line' in comment: # single-line comment elif 'line' in comment: # single-line comment
self.publish_inline_comment(comment['body'], comment['line'], comment['path']) self.publish_inline_comment(comment['body'], comment['line'], comment['path'])
else: else:
get_logger().error(f"Could not publish inline comment {comment}") get_logger().error(f"Could not publish inline comment {comment}")
@ -292,6 +417,9 @@ class BitbucketProvider(GitProvider):
def get_pr_branch(self): def get_pr_branch(self):
return self.pr.source_branch return self.pr.source_branch
def get_pr_owner_id(self) -> str | None:
return self.workspace_slug
def get_pr_description_full(self): def get_pr_description_full(self):
return self.pr.description return self.pr.description
@ -358,7 +486,6 @@ class BitbucketProvider(GitProvider):
except Exception: except Exception:
return "" return ""
def create_or_update_pr_file(self, file_path: str, branch: str, contents="", message="") -> None: 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/") url = (f"https://api.bitbucket.org/2.0/repositories/{self.workspace_slug}/{self.repo_slug}/src/")
if not message: if not message:
@ -366,30 +493,37 @@ class BitbucketProvider(GitProvider):
message = f"Update {file_path}" message = f"Update {file_path}"
else: else:
message = f"Create {file_path}" message = f"Create {file_path}"
files={file_path: contents} files = {file_path: contents}
data={ data = {
"message": message, "message": message,
"branch": branch "branch": branch
} }
headers = {'Authorization':self.headers['Authorization']} if 'Authorization' in self.headers else {} headers = {'Authorization': self.headers['Authorization']} if 'Authorization' in self.headers else {}
try: try:
requests.request("POST", url, headers=headers, data=data, files=files) requests.request("POST", url, headers=headers, data=data, files=files)
except Exception: except Exception:
get_logger().exception(f"Failed to create empty file {file_path} in branch {branch}") 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 "" try:
response = requests.request("GET", remote_link, headers=self.headers)
if response.status_code == 404: # not found
return ""
contents = response.text
return contents
except Exception:
return ""
def get_commit_messages(self): def get_commit_messages(self):
return "" # not implemented yet return "" # not implemented yet
# bitbucket does not support labels # bitbucket does not support labels
def publish_description(self, pr_title: str, description: str): def publish_description(self, pr_title: str, description: str):
payload = json.dumps({ payload = json.dumps({
"description": description, "description": description,
"title": pr_title "title": pr_title
}) })
response = requests.request("PUT", self.bitbucket_pull_request_api_url, headers=self.headers, data=payload) response = requests.request("PUT", self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
try: try:
@ -402,7 +536,7 @@ class BitbucketProvider(GitProvider):
# bitbucket does not support labels # bitbucket does not support labels
def publish_labels(self, pr_types: list): def publish_labels(self, pr_types: list):
pass pass
# bitbucket does not support labels # bitbucket does not support labels
def get_pr_labels(self, update=False): def get_pr_labels(self, update=False):
pass pass

View File

@ -1,13 +1,13 @@
import json from distutils.version import LooseVersion
from requests.exceptions import HTTPError
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import quote_plus, urlparse
import requests
from atlassian.bitbucket import Bitbucket from atlassian.bitbucket import Bitbucket
from starlette_context import context
from .git_provider import GitProvider from .git_provider import GitProvider
from pr_agent.algo.types import FilePatchInfo from ..algo.types import EDIT_TYPE, FilePatchInfo
from ..algo.language_handler import is_valid_file
from ..algo.utils import load_large_diff, find_line_number_of_relevant_line_in_file 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
@ -15,19 +15,9 @@ from ..log import get_logger
class BitbucketServerProvider(GitProvider): class BitbucketServerProvider(GitProvider):
def __init__( def __init__(
self, pr_url: Optional[str] = None, incremental: Optional[bool] = False self, pr_url: Optional[str] = None, incremental: Optional[bool] = False,
bitbucket_client: Optional[Bitbucket] = None,
): ):
s = requests.Session()
try:
bearer = context.get("bitbucket_bearer_token", None)
s.headers["Authorization"] = f"Bearer {bearer}"
except Exception:
s.headers[
"Authorization"
] = f'Bearer {get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN", None)}'
s.headers["Content-Type"] = "application/json"
self.headers = s.headers
self.bitbucket_server_url = None self.bitbucket_server_url = None
self.workspace_slug = None self.workspace_slug = None
self.repo_slug = None self.repo_slug = None
@ -41,24 +31,33 @@ class BitbucketServerProvider(GitProvider):
self.bitbucket_pull_request_api_url = pr_url self.bitbucket_pull_request_api_url = pr_url
self.bitbucket_server_url = self._parse_bitbucket_server(url=pr_url) self.bitbucket_server_url = self._parse_bitbucket_server(url=pr_url)
self.bitbucket_client = Bitbucket(url=self.bitbucket_server_url, self.bitbucket_client = bitbucket_client or Bitbucket(url=self.bitbucket_server_url,
token=get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN", None)) token=get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN",
None))
try:
self.bitbucket_api_version = LooseVersion(self.bitbucket_client.get("rest/api/1.0/application-properties").get('version'))
except Exception:
self.bitbucket_api_version = None
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
def get_repo_settings(self): def get_repo_settings(self):
try: try:
url = (f"{self.bitbucket_server_url}/projects/{self.workspace_slug}/repos/{self.repo_slug}/src/" content = self.bitbucket_client.get_content_of_file(self.workspace_slug, self.repo_slug, ".pr_agent.toml", self.get_pr_branch())
f"{self.pr.destination_branch}/.pr_agent.toml")
response = requests.request("GET", url, headers=self.headers) return content
if response.status_code == 404: # not found except Exception as e:
return "" if isinstance(e, HTTPError):
contents = response.text.encode('utf-8') if e.response.status_code == 404: # not found
return contents return ""
except Exception:
get_logger().error(f"Failed to load .pr_agent.toml file, error: {e}")
return "" return ""
def get_pr_id(self):
return self.pr_num
def publish_code_suggestions(self, code_suggestions: list) -> bool: def publish_code_suggestions(self, code_suggestions: list) -> bool:
""" """
Publishes code suggestions as comments on the PR. Publishes code suggestions as comments on the PR.
@ -87,6 +86,8 @@ class BitbucketServerProvider(GitProvider):
continue continue
if relevant_lines_end > relevant_lines_start: if relevant_lines_end > relevant_lines_start:
# Bitbucket does not support multi-line suggestions so use a code block instead - https://jira.atlassian.com/browse/BSERV-4553
body = body.replace("```suggestion", "```")
post_parameters = { post_parameters = {
"body": body, "body": body,
"path": relevant_file, "path": relevant_file,
@ -111,8 +112,11 @@ class BitbucketServerProvider(GitProvider):
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 publish_file_comments(self, file_comments: list) -> bool:
pass
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'get_labels', 'gfm_markdown']: if capability in ['get_issue_comments', 'get_labels', 'gfm_markdown', 'publish_file_comments']:
return False return False
return True return True
@ -127,7 +131,7 @@ class BitbucketServerProvider(GitProvider):
self.repo_slug, self.repo_slug,
path, path,
commit_id) commit_id)
except requests.HTTPError as e: except HTTPError as e:
get_logger().debug(f"File {path} not found at commit id: {commit_id}") get_logger().debug(f"File {path} not found at commit id: {commit_id}")
return file_content return file_content
@ -136,18 +140,50 @@ class BitbucketServerProvider(GitProvider):
diffstat = [change["path"]['toString'] for change in changes] diffstat = [change["path"]['toString'] for change in changes]
return diffstat return diffstat
#gets the best common ancestor: https://git-scm.com/docs/git-merge-base
@staticmethod
def get_best_common_ancestor(source_commits_list, destination_commits_list, guaranteed_common_ancestor) -> str:
destination_commit_hashes = {commit['id'] for commit in destination_commits_list} | {guaranteed_common_ancestor}
for commit in source_commits_list:
for parent_commit in commit['parents']:
if parent_commit['id'] in destination_commit_hashes:
return parent_commit['id']
return guaranteed_common_ancestor
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
if self.diff_files: if self.diff_files:
return self.diff_files return self.diff_files
commits_in_pr = self.bitbucket_client.get_pull_requests_commits( head_sha = self.pr.fromRef['latestCommit']
self.workspace_slug,
self.repo_slug,
self.pr_num
)
commit_list = list(commits_in_pr) # if Bitbucket api version is >= 8.16 then use the merge-base api for 2-way diff calculation
base_sha, head_sha = commit_list[0]['parents'][0]['id'], commit_list[-1]['id'] if self.bitbucket_api_version is not None and self.bitbucket_api_version >= LooseVersion("8.16"):
try:
base_sha = self.bitbucket_client.get(self._get_merge_base())['id']
except Exception as e:
get_logger().error(f"Failed to get the best common ancestor for PR: {self.pr_url}, \nerror: {e}")
raise e
else:
source_commits_list = list(self.bitbucket_client.get_pull_requests_commits(
self.workspace_slug,
self.repo_slug,
self.pr_num
))
# if Bitbucket api version is None or < 7.0 then do a simple diff with a guaranteed common ancestor
base_sha = source_commits_list[-1]['parents'][0]['id']
# if Bitbucket api version is 7.0-8.15 then use 2-way diff functionality for the base_sha
if self.bitbucket_api_version is not None and self.bitbucket_api_version >= LooseVersion("7.0"):
try:
destination_commits = list(
self.bitbucket_client.get_commits(self.workspace_slug, self.repo_slug, base_sha,
self.pr.toRef['latestCommit']))
base_sha = self.get_best_common_ancestor(source_commits_list, destination_commits, base_sha)
except Exception as e:
get_logger().error(
f"Failed to get the commit list for calculating best common ancestor for PR: {self.pr_url}, \nerror: {e}")
raise e
diff_files = [] diff_files = []
original_file_content_str = "" original_file_content_str = ""
@ -156,6 +192,10 @@ class BitbucketServerProvider(GitProvider):
changes = self.bitbucket_client.get_pull_requests_changes(self.workspace_slug, self.repo_slug, self.pr_num) changes = self.bitbucket_client.get_pull_requests_changes(self.workspace_slug, self.repo_slug, self.pr_num)
for change in changes: for change in changes:
file_path = change['path']['toString'] file_path = change['path']['toString']
if not is_valid_file(file_path.split("/")[-1]):
get_logger().info(f"Skipping a non-code file: {file_path}")
continue
match change['type']: match change['type']:
case 'ADD': case 'ADD':
edit_type = EDIT_TYPE.ADDED edit_type = EDIT_TYPE.ADDED
@ -209,7 +249,7 @@ class BitbucketServerProvider(GitProvider):
def remove_comment(self, comment): def remove_comment(self, comment):
pass pass
# funtion to create_inline_comment # function 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): absolute_position: int = None):
@ -228,7 +268,7 @@ class BitbucketServerProvider(GitProvider):
path = relevant_file.strip() path = relevant_file.strip()
return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {} return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {}
def publish_inline_comment(self, comment: str, from_line: int, file: str): def publish_inline_comment(self, comment: str, from_line: int, file: str, original_suggestion=None):
payload = { payload = {
"text": comment, "text": comment,
"severity": "NORMAL", "severity": "NORMAL",
@ -241,8 +281,18 @@ class BitbucketServerProvider(GitProvider):
} }
} }
response = requests.post(url=self._get_pr_comments_url(), json=payload, headers=self.headers) try:
return response self.bitbucket_client.post(self._get_pr_comments_path(), data=payload)
except Exception as e:
get_logger().error(f"Failed to publish inline comment to '{file}' at line {from_line}, error: {e}")
raise e
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
if relevant_line_start == -1:
link = f"{self.pr_url}/diff#{quote_plus(relevant_file)}"
else:
link = f"{self.pr_url}/diff#{quote_plus(relevant_file)}?t={relevant_line_start}"
return link
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
@ -255,18 +305,37 @@ class BitbucketServerProvider(GitProvider):
position, absolute_position = find_line_number_of_relevant_line_in_file \ position, absolute_position = find_line_number_of_relevant_line_in_file \
(diff_files, relevant_file, relevant_line_str) (diff_files, relevant_file, relevant_line_str)
if absolute_position != -1:
if self.pr:
link = f"{self.pr_url}/diff#{quote_plus(relevant_file)}?t={absolute_position}"
return link
else:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Failed adding line link to '{relevant_file}' since PR not set")
else:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Failed adding line link to '{relevant_file}' since position not found")
if absolute_position != -1 and self.pr_url: if absolute_position != -1 and self.pr_url:
link = f"{self.pr_url}/#L{relevant_file}T{absolute_position}" link = f"{self.pr_url}/diff#{quote_plus(relevant_file)}?t={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:
get_logger().info(f"Failed adding line link, error: {e}") get_logger().info(f"Failed adding line link to '{relevant_file}', error: {e}")
return "" return ""
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
for comment in comments: for comment in comments:
self.publish_inline_comment(comment['body'], comment['position'], comment['path']) if 'position' in comment:
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
elif 'start_line' in comment: # multi-line comment
# note that bitbucket does not seem to support range - only a comment on a single line - https://community.developer.atlassian.com/t/api-post-endpoint-for-inline-pull-request-comments/60452
self.publish_inline_comment(comment['body'], comment['start_line'], comment['path'])
elif 'line' in comment: # single-line comment
self.publish_inline_comment(comment['body'], comment['line'], comment['path'])
else:
get_logger().error(f"Could not publish inline comment: {comment}")
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
@ -277,8 +346,14 @@ class BitbucketServerProvider(GitProvider):
def get_pr_branch(self): def get_pr_branch(self):
return self.pr.fromRef['displayId'] return self.pr.fromRef['displayId']
def get_pr_owner_id(self) -> str | None:
return self.workspace_slug
def get_pr_description_full(self): def get_pr_description_full(self):
return self.pr.description if hasattr(self.pr, "description"):
return self.pr.description
else:
return None
def get_user_id(self): def get_user_id(self):
return 0 return 0
@ -296,16 +371,31 @@ class BitbucketServerProvider(GitProvider):
@staticmethod @staticmethod
def _parse_bitbucket_server(url: str) -> str: def _parse_bitbucket_server(url: str) -> str:
# pr url format: f"{bitbucket_server}/projects/{project_name}/repos/{repository_name}/pull-requests/{pr_id}"
parsed_url = urlparse(url) parsed_url = urlparse(url)
server_path = parsed_url.path.split("/projects/")
if len(server_path) > 1:
server_path = server_path[0].strip("/")
return f"{parsed_url.scheme}://{parsed_url.netloc}/{server_path}".strip("/")
return f"{parsed_url.scheme}://{parsed_url.netloc}" return f"{parsed_url.scheme}://{parsed_url.netloc}"
@staticmethod @staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]: def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]:
# pr url format: f"{bitbucket_server}/projects/{project_name}/repos/{repository_name}/pull-requests/{pr_id}"
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] != "pull-requests":
try:
projects_index = path_parts.index("projects")
except ValueError as e:
raise ValueError(f"The provided URL '{pr_url}' does not appear to be a Bitbucket PR URL")
path_parts = path_parts[projects_index:]
if len(path_parts) < 6 or path_parts[2] != "repos" or path_parts[4] != "pull-requests":
raise ValueError( raise ValueError(
"The provided URL does not appear to be a Bitbucket PR URL" f"The provided URL '{pr_url}' does not appear to be a Bitbucket PR URL"
) )
workspace_slug = path_parts[1] workspace_slug = path_parts[1]
@ -313,7 +403,7 @@ class BitbucketServerProvider(GitProvider):
try: try:
pr_number = int(path_parts[5]) pr_number = int(path_parts[5])
except ValueError as e: except ValueError as e:
raise ValueError("Unable to convert PR number to integer") from e raise ValueError(f"Unable to convert PR number '{path_parts[5]}' to integer") from e
return workspace_slug, repo_slug, pr_number return workspace_slug, repo_slug, pr_number
@ -323,32 +413,44 @@ class BitbucketServerProvider(GitProvider):
return self.repo return self.repo
def _get_pr(self): def _get_pr(self):
pr = self.bitbucket_client.get_pull_request(self.workspace_slug, self.repo_slug, pull_request_id=self.pr_num) try:
return type('new_dict', (object,), pr) pr = self.bitbucket_client.get_pull_request(self.workspace_slug, self.repo_slug,
pull_request_id=self.pr_num)
return type('new_dict', (object,), pr)
except Exception as e:
get_logger().error(f"Failed to get pull request, error: {e}")
raise e
def _get_pr_file_content(self, remote_link: str): def _get_pr_file_content(self, remote_link: str):
return "" return ""
def get_commit_messages(self): def get_commit_messages(self):
def get_commit_messages(self): return ""
raise NotImplementedError("Get commit messages function not implemented yet.")
# bitbucket does not support labels # bitbucket does not support labels
def publish_description(self, pr_title: str, description: str): def publish_description(self, pr_title: str, description: str):
payload = json.dumps({ payload = {
"version": self.pr.version,
"description": description, "description": description,
"title": pr_title "title": pr_title,
}) "reviewers": self.pr.reviewers # needs to be sent otherwise gets wiped
}
response = requests.put(url=self.bitbucket_pull_request_api_url, headers=self.headers, data=payload) try:
return response self.bitbucket_client.update_pull_request(self.workspace_slug, self.repo_slug, str(self.pr_num), payload)
except Exception as e:
get_logger().error(f"Failed to update pull request, error: {e}")
raise e
# bitbucket does not support labels # bitbucket does not support labels
def publish_labels(self, pr_types: list): def publish_labels(self, pr_types: list):
pass pass
# bitbucket does not support labels # bitbucket does not support labels
def get_pr_labels(self, update=False): def get_pr_labels(self, update=False):
pass pass
def _get_pr_comments_url(self): def _get_pr_comments_path(self):
return f"{self.bitbucket_server_url}/rest/api/latest/projects/{self.workspace_slug}/repos/{self.repo_slug}/pull-requests/{self.pr_num}/comments" return f"rest/api/latest/projects/{self.workspace_slug}/repos/{self.repo_slug}/pull-requests/{self.pr_num}/comments"
def _get_merge_base(self):
return f"rest/api/latest/projects/{self.workspace_slug}/repos/{self.repo_slug}/pull-requests/{self.pr_num}/merge-base"

View File

@ -225,7 +225,7 @@ class CodeCommitProvider(GitProvider):
def remove_comment(self, comment): def remove_comment(self, comment):
return "" # not implemented yet return "" # not implemented yet
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, original_suggestion=None):
# 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")

View File

@ -376,7 +376,7 @@ class GerritProvider(GitProvider):
'provider') 'provider')
def publish_inline_comment(self, body: str, relevant_file: str, def publish_inline_comment(self, body: str, relevant_file: str,
relevant_line_in_file: str): relevant_line_in_file: str, original_suggestion=None):
raise NotImplementedError( raise NotImplementedError(
'Publishing inline comments is not implemented for the gerrit ' 'Publishing inline comments is not implemented for the gerrit '
'provider') 'provider')

View File

@ -3,20 +3,28 @@ from abc import ABC, abstractmethod
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED) # enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
from typing import Optional from typing import Optional
from pr_agent.algo.utils import Range, process_description
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.algo.types import FilePatchInfo
from pr_agent.log import get_logger from pr_agent.log import get_logger
MAX_FILES_ALLOWED_FULL = 50
class GitProvider(ABC): class GitProvider(ABC):
@abstractmethod @abstractmethod
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
pass pass
@abstractmethod
def get_files(self) -> list:
pass
@abstractmethod @abstractmethod
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
pass pass
def get_incremental_commits(self, is_incremental):
pass
@abstractmethod @abstractmethod
def publish_description(self, pr_title: str, pr_body: str): def publish_description(self, pr_title: str, pr_body: str):
pass pass
@ -44,17 +52,29 @@ class GitProvider(ABC):
def edit_comment(self, comment, body: str): def edit_comment(self, comment, body: str):
pass pass
def edit_comment_from_comment_id(self, comment_id: int, body: str):
pass
def get_comment_body_from_comment_id(self, comment_id: int) -> str:
pass
def reply_to_comment_from_comment_id(self, comment_id: int, body: str): def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
pass pass
def get_pr_description(self, *, full: bool = True) -> str: def get_pr_description(self, full: bool = True, split_changes_walkthrough=False) -> str or tuple:
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
max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None) max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
description = self.get_pr_description_full() if full else self.get_user_description() description = self.get_pr_description_full() if full else self.get_user_description()
if max_tokens_description: if split_changes_walkthrough:
return clip_tokens(description, max_tokens_description) description, files = process_description(description)
return description if max_tokens_description:
description = clip_tokens(description, max_tokens_description)
return description, files
else:
if max_tokens_description:
description = clip_tokens(description, max_tokens_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): if hasattr(self, 'user_description') and not (self.user_description is None):
@ -67,6 +87,7 @@ class GitProvider(ABC):
# 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 self._is_generated_by_pr_agent(description_lowercase): if not self._is_generated_by_pr_agent(description_lowercase):
get_logger().info(f"Existing description was not generated by the pr-agent") get_logger().info(f"Existing description was not generated by the pr-agent")
self.user_description = description
return description return description
# if the existing description was generated by the pr-agent, but it doesn't contain a user description, # if the existing description was generated by the pr-agent, but it doesn't contain a user description,
@ -113,12 +134,18 @@ class GitProvider(ABC):
def get_repo_settings(self): def get_repo_settings(self):
pass pass
def get_workspace_name(self):
return ""
def get_pr_id(self): def get_pr_id(self):
return "" return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str: def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
return "" return ""
def get_lines_link_original_file(self, filepath:str, component_range: Range) -> str:
return ""
#### comments operations #### #### comments operations ####
@abstractmethod @abstractmethod
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
@ -131,8 +158,37 @@ class GitProvider(ABC):
final_update_message=True): final_update_message=True):
self.publish_comment(pr_comment) self.publish_comment(pr_comment)
def publish_persistent_comment_full(self, pr_comment: str,
initial_header: str,
update_header: bool = True,
name='review',
final_update_message=True):
try:
prev_comments = list(self.get_issue_comments())
for comment in prev_comments:
if comment.body.startswith(initial_header):
latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment)
if update_header:
updated_header = f"{initial_header}\n\n#### ({name.capitalize()} updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else:
pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
# response = self.mr.notes.update(comment.id, {'body': pr_comment_updated})
self.edit_comment(comment, pr_comment_updated)
if final_update_message:
self.publish_comment(
f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}")
return
except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}")
pass
self.publish_comment(pr_comment)
@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, original_suggestion=None):
pass pass
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,
@ -203,6 +259,9 @@ class GitProvider(ABC):
except Exception as e: except Exception as e:
return -1 return -1
def limit_output_characters(self, output: str, max_chars: int):
return output[:max_chars] + '...' if len(output) > max_chars else output
def get_main_pr_language(languages, files) -> str: def get_main_pr_language(languages, files) -> str:
""" """
@ -273,6 +332,8 @@ 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 itertools
import time import time
import hashlib import hashlib
from datetime import datetime from datetime import datetime
@ -8,24 +9,28 @@ from github import AppAuthentication, Auth, Github, GithubException
from retry import retry from retry import retry
from starlette_context import context from starlette_context import context
from ..algo.file_filter import filter_ignored
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.utils import load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file from ..algo.utils import PRReviewHeader, load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file, Range
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 GitProvider, IncrementalPR from .git_provider import GitProvider, IncrementalPR, MAX_FILES_ALLOWED_FULL
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
class GithubProvider(GitProvider): class GithubProvider(GitProvider):
def __init__(self, pr_url: Optional[str] = None, incremental=IncrementalPR(False)): def __init__(self, pr_url: Optional[str] = None):
self.repo_obj = None self.repo_obj = None
try: try:
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.max_comment_chars = 65000
self.base_url = get_settings().get("GITHUB.BASE_URL", "https://api.github.com").rstrip("/") 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.base_url_html = self.base_url.split("api/")[0].rstrip("/") if "api/" in self.base_url else "https://github.com"
self.base_domain = self.base_url.replace("https://", "").replace("http://", "")
self.base_domain_html = self.base_url_html.replace("https://", "").replace("http://", "")
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,18 +38,21 @@ class GithubProvider(GitProvider):
self.github_user_id = None self.github_user_id = None
self.diff_files = None self.diff_files = None
self.git_files = None self.git_files = None
self.incremental = incremental self.incremental = IncrementalPR(False)
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.pr_commits = list(self.pr.get_commits()) 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.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: else:
self.pr_commits = None self.pr_commits = None
def get_incremental_commits(self, incremental=IncrementalPR(False)):
self.incremental = incremental
if self.incremental.is_incremental:
self.unreviewed_files_set = dict()
self._get_incremental_commits()
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
return True return True
@ -55,7 +63,7 @@ class GithubProvider(GitProvider):
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()
def get_incremental_commits(self): def _get_incremental_commits(self):
if not self.pr_commits: if not self.pr_commits:
self.pr_commits = list(self.pr.get_commits()) self.pr_commits = list(self.pr.get_commits())
@ -92,9 +100,9 @@ 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 Review") prefixes.append(PRReviewHeader.REGULAR.value)
if incremental: if incremental:
prefixes.append("## Incremental PR Review") prefixes.append(PRReviewHeader.INCREMENTAL.value)
for index in range(len(self.comments) - 1, -1, -1): for index in range(len(self.comments) - 1, -1, -1):
if any(self.comments[index].body.startswith(prefix) for prefix in prefixes): if any(self.comments[index].body.startswith(prefix) for prefix in prefixes):
return self.comments[index] return self.comments[index]
@ -106,19 +114,22 @@ class GithubProvider(GitProvider):
git_files = context.get("git_files", None) git_files = context.get("git_files", None)
if git_files: if git_files:
return git_files return git_files
self.git_files = self.pr.get_files() self.git_files = list(self.pr.get_files()) # 'list' to handle pagination
context["git_files"] = self.git_files context["git_files"] = self.git_files
return self.git_files return self.git_files
except Exception: except Exception:
if not self.git_files: if not self.git_files:
self.git_files = self.pr.get_files() self.git_files = list(self.pr.get_files())
return self.git_files return self.git_files
def get_num_of_files(self): def get_num_of_files(self):
if self.git_files: if hasattr(self.git_files, "totalCount"):
return self.git_files.totalCount return self.git_files.totalCount
else: else:
return -1 try:
return len(self.git_files)
except Exception as e:
return -1
@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))
@ -142,22 +153,51 @@ class GithubProvider(GitProvider):
if self.diff_files: if self.diff_files:
return self.diff_files return self.diff_files
files = self.get_files() # filter files using [ignore] patterns
diff_files = [] files_original = self.get_files()
files = filter_ignored(files_original)
if files_original != files:
try:
names_original = [file.filename for file in files_original]
names_new = [file.filename for file in files]
get_logger().info(f"Filtered out [ignore] files for pull request:", extra=
{"files": names_original,
"filtered_files": names_new})
except Exception:
pass
diff_files = []
invalid_files_names = []
counter_valid = 0
for file in files: for file in files:
if not is_valid_file(file.filename): if not is_valid_file(file.filename):
invalid_files_names.append(file.filename)
continue continue
new_file_content_str = self._get_pr_file_content(file, self.pr.head.sha) # communication with GitHub
patch = file.patch patch = file.patch
# allow only a limited number of files to be fully loaded. We can manage the rest with diffs only
counter_valid += 1
avoid_load = False
if counter_valid >= MAX_FILES_ALLOWED_FULL and patch and not self.incremental.is_incremental:
avoid_load = True
if counter_valid == MAX_FILES_ALLOWED_FULL:
get_logger().info(f"Too many files in PR, will avoid loading full content for rest of files")
if avoid_load:
new_file_content_str = ""
else:
new_file_content_str = self._get_pr_file_content(file, self.pr.head.sha) # communication with GitHub
if self.incremental.is_incremental and self.unreviewed_files_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.unreviewed_files_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) if avoid_load:
original_file_content_str = ""
else:
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
if not patch: if not patch:
patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str) patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str)
@ -182,6 +222,8 @@ class GithubProvider(GitProvider):
num_plus_lines=num_plus_lines, num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines,) num_minus_lines=num_minus_lines,)
diff_files.append(file_patch_canonical_structure) diff_files.append(file_patch_canonical_structure)
if invalid_files_names:
get_logger().info(f"Filtered out files with invalid extensions: {invalid_files_names}")
self.diff_files = diff_files self.diff_files = diff_files
try: try:
@ -209,30 +251,13 @@ class GithubProvider(GitProvider):
update_header: bool = True, update_header: bool = True,
name='review', name='review',
final_update_message=True): final_update_message=True):
prev_comments = list(self.pr.get_issue_comments()) self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
for comment in prev_comments:
body = comment.body
if body.startswith(initial_header):
latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment)
if update_header:
updated_header = f"{initial_header}\n\n### ({name.capitalize()} updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else:
pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
response = comment.edit(pr_comment_updated)
if final_update_message:
self.publish_comment(
f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}")
return
self.publish_comment(pr_comment)
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if is_temporary and not get_settings().config.publish_output_progress: if is_temporary and not get_settings().config.publish_output_progress:
get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}") get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
return return
pr_comment = self.limit_output_characters(pr_comment, self.max_comment_chars)
response = self.pr.create_issue_comment(pr_comment) response = self.pr.create_issue_comment(pr_comment)
if hasattr(response, "user") and hasattr(response.user, "login"): if hasattr(response, "user") and hasattr(response.user, "login"):
self.github_user_id = response.user.login self.github_user_id = response.user.login
@ -242,12 +267,14 @@ class GithubProvider(GitProvider):
self.pr.comments_list.append(response) self.pr.comments_list.append(response)
return 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, original_suggestion=None):
body = self.limit_output_characters(body, self.max_comment_chars)
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,
absolute_position: int = None): absolute_position: int = None):
body = self.limit_output_characters(body, self.max_comment_chars)
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files,
relevant_file.strip('`'), relevant_file.strip('`'),
relevant_line_in_file, relevant_line_in_file,
@ -420,11 +447,24 @@ class GithubProvider(GitProvider):
return False return False
def edit_comment(self, comment, body: str): def edit_comment(self, comment, body: str):
body = self.limit_output_characters(body, self.max_comment_chars)
comment.edit(body=body) comment.edit(body=body)
def edit_comment_from_comment_id(self, comment_id: int, body: str):
try:
# self.pr.get_issue_comment(comment_id).edit(body)
body = self.limit_output_characters(body, self.max_comment_chars)
headers, data_patch = self.pr._requester.requestJsonAndCheck(
"PATCH", f"{self.base_url}/repos/{self.repo}/issues/comments/{comment_id}",
input={"body": body}
)
except Exception as e:
get_logger().exception(f"Failed to edit comment, error: {e}")
def reply_to_comment_from_comment_id(self, comment_id: int, body: str): def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
try: try:
# self.pr.get_issue_comment(comment_id).edit(body) # self.pr.get_issue_comment(comment_id).edit(body)
body = self.limit_output_characters(body, self.max_comment_chars)
headers, data_patch = self.pr._requester.requestJsonAndCheck( headers, data_patch = self.pr._requester.requestJsonAndCheck(
"POST", f"{self.base_url}/repos/{self.repo}/pulls/{self.pr_num}/comments/{comment_id}/replies", "POST", f"{self.base_url}/repos/{self.repo}/pulls/{self.pr_num}/comments/{comment_id}/replies",
input={"body": body} input={"body": body}
@ -432,6 +472,51 @@ class GithubProvider(GitProvider):
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to reply comment, error: {e}") get_logger().exception(f"Failed to reply comment, error: {e}")
def get_comment_body_from_comment_id(self, comment_id: int):
try:
# self.pr.get_issue_comment(comment_id).edit(body)
headers, data_patch = self.pr._requester.requestJsonAndCheck(
"GET", f"{self.base_url}/repos/{self.repo}/issues/comments/{comment_id}"
)
return data_patch.get("body","")
except Exception as e:
get_logger().exception(f"Failed to edit comment, error: {e}")
return None
def publish_file_comments(self, file_comments: list) -> bool:
try:
headers, existing_comments = self.pr._requester.requestJsonAndCheck(
"GET", f"{self.pr.url}/comments"
)
for comment in file_comments:
comment['commit_id'] = self.last_commit_id.sha
comment['body'] = self.limit_output_characters(comment['body'], self.max_comment_chars)
found = False
for existing_comment in existing_comments:
comment['commit_id'] = self.last_commit_id.sha
our_app_name = get_settings().get("GITHUB.APP_NAME", "")
same_comment_creator = False
if self.deployment_type == 'app':
same_comment_creator = our_app_name.lower() in existing_comment['user']['login'].lower()
elif self.deployment_type == 'user':
same_comment_creator = self.github_user_id == existing_comment['user']['login']
if existing_comment['subject_type'] == 'file' and comment['path'] == existing_comment['path'] and same_comment_creator:
headers, data_patch = self.pr._requester.requestJsonAndCheck(
"PATCH", f"{self.base_url}/repos/{self.repo}/pulls/comments/{existing_comment['id']}", input={"body":comment['body']}
)
found = True
break
if not found:
headers, data_post = self.pr._requester.requestJsonAndCheck(
"POST", f"{self.pr.url}/comments", input=comment
)
return True
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish diffview file summary, error: {e}")
return False
def remove_initial_comment(self): def remove_initial_comment(self):
try: try:
for comment in getattr(self.pr, 'comments_list', []): for comment in getattr(self.pr, 'comments_list', []):
@ -456,6 +541,11 @@ class GithubProvider(GitProvider):
def get_pr_branch(self): def get_pr_branch(self):
return self.pr.head.ref return self.pr.head.ref
def get_pr_owner_id(self) -> str | None:
if not self.repo:
return None
return self.repo.split('/')[0]
def get_pr_description_full(self): def get_pr_description_full(self):
return self.pr.body return self.pr.body
@ -490,6 +580,9 @@ class GithubProvider(GitProvider):
except Exception: except Exception:
return "" return ""
def get_workspace_name(self):
return self.repo.split('/')[0]
def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
if disable_eyes: if disable_eyes:
return None return None
@ -500,7 +593,7 @@ class GithubProvider(GitProvider):
) )
return data_patch.get("id", None) 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().warning(f"Failed to add eyes reaction, error: {e}")
return None return None
def remove_reaction(self, issue_comment_id: int, reaction_id: str) -> bool: def remove_reaction(self, issue_comment_id: int, reaction_id: str) -> bool:
@ -515,15 +608,11 @@ class GithubProvider(GitProvider):
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 def _parse_pr_url(self, pr_url: str) -> Tuple[str, int]:
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url) parsed_url = urlparse(pr_url)
if 'github.com' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid GitHub URL")
path_parts = parsed_url.path.strip('/').split('/') path_parts = parsed_url.path.strip('/').split('/')
if 'api.github.com' in parsed_url.netloc: if self.base_domain in parsed_url.netloc:
if len(path_parts) < 5 or path_parts[3] != 'pulls': if len(path_parts) < 5 or path_parts[3] != 'pulls':
raise ValueError("The provided URL does not appear to be a GitHub PR URL") raise ValueError("The provided URL does not appear to be a GitHub PR URL")
repo_name = '/'.join(path_parts[1:3]) repo_name = '/'.join(path_parts[1:3])
@ -544,15 +633,10 @@ class GithubProvider(GitProvider):
return repo_name, pr_number return repo_name, pr_number
@staticmethod def _parse_issue_url(self, issue_url: str) -> Tuple[str, int]:
def _parse_issue_url(issue_url: str) -> Tuple[str, int]:
parsed_url = urlparse(issue_url) parsed_url = urlparse(issue_url)
if 'github.com' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid GitHub URL")
path_parts = parsed_url.path.strip('/').split('/') path_parts = parsed_url.path.strip('/').split('/')
if 'api.github.com' in parsed_url.netloc: if self.base_domain in parsed_url.netloc:
if len(path_parts) < 5 or path_parts[3] != 'issues': if len(path_parts) < 5 or path_parts[3] != 'issues':
raise ValueError("The provided URL does not appear to be a GitHub ISSUE URL") raise ValueError("The provided URL does not appear to be a GitHub ISSUE URL")
repo_name = '/'.join(path_parts[1:3]) repo_name = '/'.join(path_parts[1:3])
@ -653,7 +737,7 @@ class GithubProvider(GitProvider):
"PUT", f"{self.pr.issue_url}/labels", input=post_parameters "PUT", f"{self.pr.issue_url}/labels", input=post_parameters
) )
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}") get_logger().warning(f"Failed to publish labels, error: {e}")
def get_pr_labels(self, update=False): def get_pr_labels(self, update=False):
try: try:
@ -671,7 +755,7 @@ class GithubProvider(GitProvider):
def get_repo_labels(self): def get_repo_labels(self):
labels = self.repo_obj.get_labels() labels = self.repo_obj.get_labels()
return [label for label in labels] return [label for label in itertools.islice(labels, 50)]
def get_commit_messages(self): def get_commit_messages(self):
""" """
@ -726,6 +810,32 @@ class GithubProvider(GitProvider):
link = f"{self.base_url_html}/{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
def get_lines_link_original_file(self, filepath: str, component_range: Range) -> str:
"""
Returns the link to the original file on GitHub that corresponds to the given filepath and component range.
Args:
filepath (str): The path of the file.
component_range (Range): The range of lines that represent the component.
Returns:
str: The link to the original file on GitHub.
Example:
>>> filepath = "path/to/file.py"
>>> component_range = Range(line_start=10, line_end=20)
>>> link = get_lines_link_original_file(filepath, component_range)
>>> print(link)
"https://github.com/{repo}/blob/{commit_sha}/{filepath}/#L11-L21"
"""
line_start = component_range.line_start + 1
line_end = component_range.line_end + 1
# link = (f"https://github.com/{self.repo}/blob/{self.last_commit_id.sha}/{filepath}/"
# f"#L{line_start}-L{line_end}")
link = (f"{self.base_url_html}/{self.repo}/blob/{self.last_commit_id.sha}/{filepath}/"
f"#L{line_start}-L{line_end}")
return link
def get_pr_id(self): def get_pr_id(self):
try: try:
@ -745,4 +855,4 @@ class GithubProvider(GitProvider):
return False return False
def calc_pr_statistics(self, pull_request_data: dict): def calc_pr_statistics(self, pull_request_data: dict):
return {} return {}

View File

@ -4,12 +4,14 @@ from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
import gitlab import gitlab
import requests
from gitlab import GitlabGetError from gitlab import GitlabGetError
from ..algo.file_filter import filter_ignored
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_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, find_line_number_of_relevant_line_in_file
from ..config_loader import get_settings from ..config_loader import get_settings
from .git_provider import GitProvider from .git_provider import GitProvider, MAX_FILES_ALLOWED_FULL
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
from ..log import get_logger from ..log import get_logger
@ -24,6 +26,7 @@ class GitLabProvider(GitProvider):
gitlab_url = get_settings().get("GITLAB.URL", None) gitlab_url = get_settings().get("GITLAB.URL", None)
if not gitlab_url: if not gitlab_url:
raise ValueError("GitLab URL is not set in the config file") raise ValueError("GitLab URL is not set in the config file")
self.gitlab_url = gitlab_url
gitlab_access_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None) gitlab_access_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
if not gitlab_access_token: if not gitlab_access_token:
raise ValueError("GitLab personal access token is not set in the config file") raise ValueError("GitLab personal access token is not set in the config file")
@ -31,6 +34,7 @@ class GitLabProvider(GitProvider):
url=gitlab_url, url=gitlab_url,
oauth_token=gitlab_access_token oauth_token=gitlab_access_token
) )
self.max_comment_chars = 65000
self.id_project = None self.id_project = None
self.id_mr = None self.id_mr = None
self.mr = None self.mr = None
@ -44,7 +48,8 @@ class GitLabProvider(GitProvider):
self.incremental = incremental self.incremental = incremental
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']: # gfm_markdown is supported in gitlab ! if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments',
'publish_file_comments']: # gfm_markdown is supported in gitlab !
return False return False
return True return True
@ -84,53 +89,81 @@ class GitLabProvider(GitProvider):
if self.diff_files: if self.diff_files:
return self.diff_files return self.diff_files
diffs = self.mr.changes()['changes'] # filter files using [ignore] patterns
diffs_original = self.mr.changes()['changes']
diffs = filter_ignored(diffs_original, 'gitlab')
if diffs != diffs_original:
try:
names_original = [diff['new_path'] for diff in diffs_original]
names_filtered = [diff['new_path'] for diff in diffs]
get_logger().info(f"Filtered out [ignore] files for merge request {self.id_mr}", extra={
'original_files': names_original,
'filtered_files': names_filtered
})
except Exception as e:
pass
diff_files = [] diff_files = []
invalid_files_names = []
counter_valid = 0
for diff in diffs: for diff in diffs:
if is_valid_file(diff['new_path']): if not is_valid_file(diff['new_path']):
invalid_files_names.append(diff['new_path'])
continue
# allow only a limited number of files to be fully loaded. We can manage the rest with diffs only
counter_valid += 1
if counter_valid < MAX_FILES_ALLOWED_FULL or not diff['diff']:
original_file_content_str = self.get_pr_file_content(diff['old_path'], self.mr.diff_refs['base_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']) new_file_content_str = self.get_pr_file_content(diff['new_path'], self.mr.diff_refs['head_sha'])
else:
if counter_valid == MAX_FILES_ALLOWED_FULL:
get_logger().info(f"Too many files in PR, will avoid loading full content for rest of files")
original_file_content_str = ''
new_file_content_str = ''
try: try:
if isinstance(original_file_content_str, bytes): if isinstance(original_file_content_str, bytes):
original_file_content_str = bytes.decode(original_file_content_str, 'utf-8') original_file_content_str = bytes.decode(original_file_content_str, 'utf-8')
if isinstance(new_file_content_str, bytes): if isinstance(new_file_content_str, bytes):
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8') new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
get_logger().warning( get_logger().warning(
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}") f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
edit_type = EDIT_TYPE.MODIFIED edit_type = EDIT_TYPE.MODIFIED
if diff['new_file']: if diff['new_file']:
edit_type = EDIT_TYPE.ADDED edit_type = EDIT_TYPE.ADDED
elif diff['deleted_file']: elif diff['deleted_file']:
edit_type = EDIT_TYPE.DELETED edit_type = EDIT_TYPE.DELETED
elif diff['renamed_file']: elif diff['renamed_file']:
edit_type = EDIT_TYPE.RENAMED edit_type = EDIT_TYPE.RENAMED
filename = diff['new_path'] filename = diff['new_path']
patch = diff['diff'] patch = diff['diff']
if not patch: if not patch:
patch = load_large_diff(filename, new_file_content_str, original_file_content_str) patch = load_large_diff(filename, new_file_content_str, original_file_content_str)
# count number of lines added and removed # count number of lines added and removed
patch_lines = patch.splitlines(keepends=True) patch_lines = patch.splitlines(keepends=True)
num_plus_lines = len([line for line in patch_lines if line.startswith('+')]) num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
num_minus_lines = len([line for line in patch_lines if line.startswith('-')]) num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
diff_files.append( diff_files.append(
FilePatchInfo(original_file_content_str, new_file_content_str, FilePatchInfo(original_file_content_str, new_file_content_str,
patch=patch, patch=patch,
filename=filename, filename=filename,
edit_type=edit_type, edit_type=edit_type,
old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path'], old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path'],
num_plus_lines=num_plus_lines, num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines, )) num_minus_lines=num_minus_lines, ))
if invalid_files_names:
get_logger().info(f"Filtered out files with invalid extensions: {invalid_files_names}")
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
def get_files(self): def get_files(self) -> list:
if not self.git_files: if not self.git_files:
self.git_files = [change['new_path'] for change in self.mr.changes()['changes']] self.git_files = [change['new_path'] for change in self.mr.changes()['changes']]
return self.git_files return self.git_files
@ -154,45 +187,36 @@ class GitLabProvider(GitProvider):
update_header: bool = True, update_header: bool = True,
name='review', name='review',
final_update_message=True): final_update_message=True):
try: self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
for comment in self.mr.notes.list(get_all=True)[::-1]:
if comment.body.startswith(initial_header):
latest_commit_url = self.get_latest_commit_url()
comment_url = self.get_comment_url(comment)
if update_header:
updated_header = f"{initial_header}\n\n### ({name.capitalize()} updated until commit {latest_commit_url})\n"
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
else:
pr_comment_updated = pr_comment
get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
response = self.mr.notes.update(comment.id, {'body': pr_comment_updated})
if final_update_message:
self.publish_comment(
f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}")
return
except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}")
pass
self.publish_comment(pr_comment)
def publish_comment(self, mr_comment: str, is_temporary: bool = False): def publish_comment(self, mr_comment: str, is_temporary: bool = False):
mr_comment = self.limit_output_characters(mr_comment, self.max_comment_chars)
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 return comment
def edit_comment(self, comment, body: str): def edit_comment(self, comment, body: str):
body = self.limit_output_characters(body, self.max_comment_chars)
self.mr.notes.update(comment.id,{'body': body} ) self.mr.notes.update(comment.id,{'body': body} )
def edit_comment_from_comment_id(self, comment_id: int, body: str):
body = self.limit_output_characters(body, self.max_comment_chars)
comment = self.mr.notes.get(comment_id)
comment.body = body
comment.save()
def reply_to_comment_from_comment_id(self, comment_id: int, body: str): def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
body = self.limit_output_characters(body, self.max_comment_chars)
discussion = self.mr.discussions.get(comment_id) discussion = self.mr.discussions.get(comment_id)
discussion.notes.create({'body': body}) 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, original_suggestion=None):
body = self.limit_output_characters(body, self.max_comment_chars)
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,
relevant_line_in_file) relevant_line_in_file)
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, original_suggestion)
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): 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")
@ -200,8 +224,14 @@ class GitLabProvider(GitProvider):
def create_inline_comments(self, comments: list[dict]): def create_inline_comments(self, comments: list[dict]):
raise NotImplementedError("Gitlab provider does not support publishing inline comments yet") raise NotImplementedError("Gitlab provider does not support publishing inline comments yet")
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int, def get_comment_body_from_comment_id(self, comment_id: int):
source_line_no: int, target_file: str,target_line_no: int) -> None: comment = self.mr.notes.get(comment_id).body
return comment
def send_inline_comment(self, body: str, edit_type: str, found: bool, relevant_file: str,
relevant_line_in_file: str,
source_line_no: int, target_file: str, target_line_no: int,
original_suggestion=None) -> None:
if not found: if not found:
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}")
else: else:
@ -221,14 +251,63 @@ class GitLabProvider(GitProvider):
else: else:
pos_obj['new_line'] = target_line_no - 1 pos_obj['new_line'] = target_line_no - 1
pos_obj['old_line'] = source_line_no - 1 pos_obj['old_line'] = source_line_no - 1
get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") get_logger().debug(f"Creating comment in MR {self.id_mr} with body {body} and position {pos_obj}")
try: try:
self.mr.discussions.create({'body': body, 'position': pos_obj}) self.mr.discussions.create({'body': body, 'position': pos_obj})
except Exception as e: except Exception as e:
get_logger().debug( try:
f"Failed to create comment in {self.id_mr} with position {pos_obj} (probably not a '+' line)") # fallback - create a general note on the file in the MR
if 'suggestion_orig_location' in original_suggestion:
line_start = original_suggestion['suggestion_orig_location']['start_line']
line_end = original_suggestion['suggestion_orig_location']['end_line']
old_code_snippet = original_suggestion['prev_code_snippet']
new_code_snippet = original_suggestion['new_code_snippet']
content = original_suggestion['suggestion_summary']
label = original_suggestion['category']
if 'score' in original_suggestion:
score = original_suggestion['score']
else:
score = 7
else:
line_start = original_suggestion['relevant_lines_start']
line_end = original_suggestion['relevant_lines_end']
old_code_snippet = original_suggestion['existing_code']
new_code_snippet = original_suggestion['improved_code']
content = original_suggestion['suggestion_content']
label = original_suggestion['label']
if 'score' in original_suggestion:
score = original_suggestion['score']
else:
score = 7
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: if hasattr(self, 'main_language'):
language = self.main_language
else:
language = ''
link = self.get_line_link(relevant_file, line_start, line_end)
body_fallback =f"**Suggestion:** {content} [{label}, importance: {score}]\n___\n"
body_fallback +=f"\n\nReplace lines ([{line_start}-{line_end}]({link}))\n\n```{language}\n{old_code_snippet}\n````\n\n"
body_fallback +=f"with\n\n```{language}\n{new_code_snippet}\n````"
body_fallback += f"\n\n___\n\n`(Cannot implement this suggestion directly, as gitlab API does not enable committing to a non -+ line in a PR)`"
# Create a general note on the file in the MR
self.mr.notes.create({
'body': body_fallback,
'position': {
'base_sha': diff.base_commit_sha,
'start_sha': diff.start_commit_sha,
'head_sha': diff.head_commit_sha,
'position_type': 'text',
'file_path': f'{target_file.filename}',
}
})
# get_logger().debug(
# f"Failed to create comment in MR {self.id_mr} with position {pos_obj} (probably not a '+' line)")
except Exception as e:
get_logger().exception(f"Failed to create comment in MR {self.id_mr}")
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: str) -> Optional[dict]:
changes = self.mr.changes() # Retrieve the changes for the merge request once changes = self.mr.changes() # Retrieve the changes for the merge request once
if not changes: if not changes:
get_logger().error('No changes found for the merge request.') get_logger().error('No changes found for the merge request.')
@ -248,6 +327,10 @@ class GitLabProvider(GitProvider):
def publish_code_suggestions(self, code_suggestions: list) -> bool: def publish_code_suggestions(self, code_suggestions: list) -> bool:
for suggestion in code_suggestions: for suggestion in code_suggestions:
try: try:
if suggestion and 'original_suggestion' in suggestion:
original_suggestion = suggestion['original_suggestion']
else:
original_suggestion = suggestion
body = suggestion['body'] body = suggestion['body']
relevant_file = suggestion['relevant_file'] relevant_file = suggestion['relevant_file']
relevant_lines_start = suggestion['relevant_lines_start'] relevant_lines_start = suggestion['relevant_lines_start']
@ -268,19 +351,22 @@ class GitLabProvider(GitProvider):
# edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file, # edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file,
# relevant_line_in_file) # relevant_line_in_file)
# for code suggestions, we want to edit the new code # for code suggestions, we want to edit the new code
source_line_no = None source_line_no = -1
target_line_no = relevant_lines_start + 1 target_line_no = relevant_lines_start + 1
found = True found = True
edit_type = 'addition' edit_type = 'addition'
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, original_suggestion)
except Exception as e: except Exception as e:
get_logger().exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}") get_logger().exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}")
# note that we publish suggestions one-by-one. so, if one fails, the rest will still be published # note that we publish suggestions one-by-one. so, if one fails, the rest will still be published
return True return True
def publish_file_comments(self, file_comments: list) -> bool:
pass
def search_line(self, relevant_file, relevant_line_in_file): def search_line(self, relevant_file, relevant_line_in_file):
target_file = None target_file = None
@ -358,11 +444,20 @@ class GitLabProvider(GitProvider):
def get_pr_branch(self): def get_pr_branch(self):
return self.mr.source_branch return self.mr.source_branch
def get_pr_owner_id(self) -> str | None:
if not self.gitlab_url or 'gitlab.com' in self.gitlab_url:
if not self.id_project:
return None
return self.id_project.split('/')[0]
# extract host name
host = urlparse(self.gitlab_url).hostname
return host
def get_pr_description_full(self): def get_pr_description_full(self):
return self.mr.description return self.mr.description
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError("GitLab provider does not support issue comments yet") return self.mr.notes.list(get_all=True)[::-1]
def get_repo_settings(self): def get_repo_settings(self):
try: try:
@ -371,6 +466,9 @@ class GitLabProvider(GitProvider):
except Exception: except Exception:
return "" return ""
def get_workspace_name(self):
return self.id_project.split('/')[0]
def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
return True return True
@ -414,7 +512,7 @@ class GitLabProvider(GitProvider):
self.mr.labels = list(set(pr_types)) self.mr.labels = list(set(pr_types))
self.mr.save() self.mr.save()
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}") get_logger().warning(f"Failed to publish labels, error: {e}")
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
pass pass
@ -453,7 +551,7 @@ class GitLabProvider(GitProvider):
if relevant_line_start == -1: if relevant_line_start == -1:
link = f"{self.gl.url}/{self.id_project}/-/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"{self.gl.url}/{self.id_project}/-/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}-{relevant_line_end}"
else: else:
link = f"{self.gl.url}/{self.id_project}/-/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

View File

@ -119,7 +119,7 @@ class LocalGitProvider(GitProvider):
# Write the string to the file # Write the string to the file
file.write(pr_comment) file.write(pr_comment)
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, original_suggestion=None):
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 publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):

View File

@ -5,12 +5,13 @@ import tempfile
from dynaconf import Dynaconf 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, get_git_provider_with_context
from pr_agent.log import get_logger from pr_agent.log import get_logger
from starlette_context import context from starlette_context import context
def apply_repo_settings(pr_url): def apply_repo_settings(pr_url):
git_provider = get_git_provider_with_context(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:
@ -20,7 +21,6 @@ def apply_repo_settings(pr_url):
repo_settings = None repo_settings = None
pass pass
if repo_settings is None: # None is different from "", which is a valid value 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() repo_settings = git_provider.get_repo_settings()
try: try:
context["repo_settings"] = repo_settings context["repo_settings"] = repo_settings
@ -47,3 +47,17 @@ def apply_repo_settings(pr_url):
os.remove(repo_settings_file) os.remove(repo_settings_file)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to remove temporary settings file {repo_settings_file}", e) get_logger().error(f"Failed to remove temporary settings file {repo_settings_file}", e)
# enable switching models with a short definition
if get_settings().config.model.lower()=='claude-3-5-sonnet':
set_claude_model()
def set_claude_model():
"""
set the claude-sonnet-3.5 model easily (even by users), just by stating: --config.model='claude-3-5-sonnet'
"""
model_claude = "bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0"
get_settings().set('config.model', model_claude)
get_settings().set('config.model_turbo', model_claude)
get_settings().set('config.fallback_models', [model_claude])

View File

@ -22,7 +22,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
blob = self.bucket.blob(secret_name) blob = self.bucket.blob(secret_name)
return blob.download_as_string() return blob.download_as_string()
except Exception as e: except Exception as e:
get_logger().error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}") get_logger().warning(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}")
return "" return ""
def store_secret(self, secret_name: str, secret_value: str): def store_secret(self, secret_name: str, secret_value: str):

View File

@ -26,8 +26,9 @@ 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 fastapi import Request, Depends from fastapi import Request, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pr_agent.log import get_logger from pr_agent.log import LoggingFormat, get_logger, setup_logger
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
security = HTTPBasic() security = HTTPBasic()
router = APIRouter() router = APIRouter()
available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*") available_commands_rgx = re.compile(r"^\/(" + "|".join(command2class.keys()) + r")\s*")
@ -40,8 +41,15 @@ def handle_request(
): ):
log_context["action"] = body log_context["action"] = body
log_context["api_url"] = url log_context["api_url"] = url
with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body) async def inner():
try:
with get_logger().contextualize(**log_context):
await PRAgent().handle_request(url, body)
except Exception as e:
get_logger().error(f"Failed to handle webhook: {e}")
background_tasks.add_task(inner)
# currently only basic auth is supported with azure webhooks # currently only basic auth is supported with azure webhooks
@ -60,6 +68,7 @@ def authorize(credentials: HTTPBasicCredentials = Depends(security)):
async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict): async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
apply_repo_settings(api_url) apply_repo_settings(api_url)
commands = get_settings().get(f"azure_devops_server.{commands_conf}") commands = get_settings().get(f"azure_devops_server.{commands_conf}")
get_settings().set("config.is_auto_command", True)
for command in commands: for command in commands:
try: try:
split_command = command.split(" ") split_command = command.split(" ")
@ -81,7 +90,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
get_logger().info(json.dumps(data)) get_logger().info(json.dumps(data))
actions = [] actions = []
if data["eventType"] == "git.pullrequest.created": if data["eventType"] == "git.pullrequest.created":
# API V1 (latest) # API V1 (latest)
pr_url = unquote(data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git")) pr_url = unquote(data["resource"]["_links"]["web"]["href"].replace("_apis/git/repositories", "_git"))
log_context["event"] = data["eventType"] log_context["event"] = data["eventType"]
@ -94,7 +103,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
repo = data["resource"]["pullRequest"]["repository"]["webUrl"] repo = data["resource"]["pullRequest"]["repository"]["webUrl"]
pr_url = unquote(f'{repo}/pullrequest/{data["resource"]["pullRequest"]["pullRequestId"]}') pr_url = unquote(f'{repo}/pullrequest/{data["resource"]["pullRequest"]["pullRequestId"]}')
actions = [data["resource"]["comment"]["content"]] actions = [data["resource"]["comment"]["content"]]
else: else:
# API V1 not supported as it does not contain the PR URL # API V1 not supported as it does not contain the PR URL
return JSONResponse( return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -112,7 +121,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context["event"] = data["eventType"] log_context["event"] = data["eventType"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
for action in actions: for action in actions:
try: try:
handle_request(background_tasks, pr_url, action, log_context) handle_request(background_tasks, pr_url, action, log_context)
@ -123,13 +132,13 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
content=json.dumps({"message": "Internal server error"}), content=json.dumps({"message": "Internal server error"}),
) )
return JSONResponse( return JSONResponse(
status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggerd successfully"}) status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggered successfully"})
) )
@router.get("/") @router.get("/")
async def root(): async def root():
return {"status": "ok"} return {"status": "ok"}
def start(): def start():
app = FastAPI(middleware=[Middleware(RawContextMiddleware)]) app = FastAPI(middleware=[Middleware(RawContextMiddleware)])
app.include_router(router) app.include_router(router)

View File

@ -3,6 +3,7 @@ import copy
import hashlib import hashlib
import json import json
import os import os
import re
import time import time
import jwt import jwt
@ -77,6 +78,7 @@ async def handle_manifest(request: Request, response: Response):
async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict): async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
apply_repo_settings(api_url) apply_repo_settings(api_url)
commands = get_settings().get(f"bitbucket_app.{commands_conf}", {}) commands = get_settings().get(f"bitbucket_app.{commands_conf}", {})
get_settings().set("config.is_auto_command", True)
for command in commands: for command in commands:
try: try:
split_command = command.split(" ") split_command = command.split(" ")
@ -91,29 +93,81 @@ async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_ur
get_logger().error(f"Failed to perform command {command}: {e}") get_logger().error(f"Failed to perform command {command}: {e}")
def is_bot_user(data) -> bool:
try:
if data["data"]["actor"]["type"] != "user":
get_logger().info(f"BitBucket actor type is not 'user': {data['data']['actor']['type']}")
return True
except Exception as e:
get_logger().error("Failed 'is_bot_user' logic: {e}")
return False
def should_process_pr_logic(data) -> bool:
try:
pr_data = data.get("data", {}).get("pullrequest", {})
title = pr_data.get("title", "")
source_branch = pr_data.get("source", {}).get("branch", {}).get("name", "")
target_branch = pr_data.get("destination", {}).get("branch", {}).get("name", "")
# logic to ignore PRs with specific titles
if title:
ignore_pr_title_re = get_settings().get("CONFIG.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 config.ignore_pr_title setting")
return False
ignore_pr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
if (ignore_pr_source_branches or ignore_pr_target_branches):
if any(re.search(regex, source_branch) for regex in ignore_pr_source_branches):
get_logger().info(
f"Ignoring PR with source branch '{source_branch}' due to config.ignore_pr_source_branches settings")
return False
if any(re.search(regex, target_branch) for regex in ignore_pr_target_branches):
get_logger().info(
f"Ignoring PR with target branch '{target_branch}' due to config.ignore_pr_target_branches settings")
return False
except Exception as e:
get_logger().error(f"Failed 'should_process_pr_logic': {e}")
return True
@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"} app_name = get_settings().get("CONFIG.APP_NAME", "Unknown")
log_context = {"server_type": "bitbucket_app", "app_name": app_name}
get_logger().debug(request.headers) get_logger().debug(request.headers)
jwt_header = request.headers.get("authorization", None) jwt_header = request.headers.get("authorization", None)
if jwt_header: if jwt_header:
input_jwt = jwt_header.split(" ")[1] input_jwt = jwt_header.split(" ")[1]
data = await request.json() data = await request.json()
get_logger().debug(data) get_logger().debug(data)
async def inner(): async def inner():
try: try:
try: # ignore bot users
if data["data"]["actor"]["type"] != "user": if is_bot_user(data):
return "OK"
# Check if the PR should be processed
if data.get("event", "") == "pullrequest:created":
if not should_process_pr_logic(data):
return "OK" return "OK"
except KeyError:
get_logger().error("Failed to get actor type, check previous logs, this shouldn't happen.") # Get the username of the sender
try: try:
owner = data["data"]["repository"]["owner"]["username"] username = data["data"]["actor"]["username"]
except Exception as e: except KeyError:
get_logger().error(f"Failed to get owner, will continue: {e}") try:
owner = "unknown" username = data["data"]["actor"]["display_name"]
except KeyError:
username = data["data"]["actor"]["nickname"]
log_context["sender"] = username
sender_id = data["data"]["actor"]["account_id"] sender_id = data["data"]["actor"]["account_id"]
log_context["sender"] = owner
log_context["sender_id"] = sender_id log_context["sender_id"] = sender_id
jwt_parts = input_jwt.split(".") jwt_parts = input_jwt.split(".")
claim_part = jwt_parts[1] claim_part = jwt_parts[1]
@ -140,16 +194,6 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
sender_id, pr_url) is not Eligibility.NOT_ELIGIBLE: sender_id, pr_url) is not Eligibility.NOT_ELIGIBLE:
if get_settings().get("bitbucket_app.pr_commands"): if get_settings().get("bitbucket_app.pr_commands"):
await _perform_commands_bitbucket("pr_commands", PRAgent(), pr_url, log_context) 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

View File

@ -1,5 +1,7 @@
import ast
import json import json
import os import os
from typing import List
import uvicorn import uvicorn
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
@ -10,12 +12,16 @@ from starlette.middleware import Middleware
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
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 from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.servers.utils import verify_signature from pr_agent.servers.utils import verify_signature
from fastapi.responses import RedirectResponse
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
router = APIRouter() router = APIRouter()
@ -24,11 +30,21 @@ def handle_request(
): ):
log_context["action"] = body log_context["action"] = body
log_context["api_url"] = url log_context["api_url"] = url
with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body)
async def inner():
try:
with get_logger().contextualize(**log_context):
await PRAgent().handle_request(url, body)
except Exception as e:
get_logger().error(f"Failed to handle webhook: {e}")
background_tasks.add_task(inner)
@router.post("/") @router.post("/")
async def redirect_to_webhook():
return RedirectResponse(url="/webhook")
@router.post("/webhook")
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()
@ -37,6 +53,10 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
webhook_secret = get_settings().get("BITBUCKET_SERVER.WEBHOOK_SECRET", None) webhook_secret = get_settings().get("BITBUCKET_SERVER.WEBHOOK_SECRET", None)
if webhook_secret: if webhook_secret:
body_bytes = await request.body() body_bytes = await request.body()
if body_bytes.decode('utf-8') == '{"test": true}':
return JSONResponse(
status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "connection test successful"})
)
signature_header = request.headers.get("x-hub-signature", None) signature_header = request.headers.get("x-hub-signature", None)
verify_signature(body_bytes, webhook_secret, signature_header) verify_signature(body_bytes, webhook_secret, signature_header)
@ -49,22 +69,81 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
log_context["event"] = "pull_request" log_context["event"] = "pull_request"
commands_to_run = []
if data["eventKey"] == "pr:opened": if data["eventKey"] == "pr:opened":
body = "review" commands_to_run.extend(_get_commands_list_from_settings('BITBUCKET_SERVER.PR_COMMANDS'))
elif data["eventKey"] == "pr:comment:added": elif data["eventKey"] == "pr:comment:added":
body = data["comment"]["text"] commands_to_run.append(data["comment"]["text"])
else: else:
return JSONResponse( return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
content=json.dumps({"message": "Unsupported event"}), content=json.dumps({"message": "Unsupported event"}),
) )
handle_request(background_tasks, pr_url, body, log_context) async def inner():
try:
await _run_commands_sequentially(commands_to_run, pr_url, log_context)
except Exception as e:
get_logger().error(f"Failed to handle webhook: {e}")
background_tasks.add_task(inner)
return JSONResponse( return JSONResponse(
status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}) status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"})
) )
async def _run_commands_sequentially(commands: List[str], url: str, log_context: dict):
get_logger().info(f"Running commands sequentially: {commands}")
if commands is None:
return
for command in commands:
try:
body = _process_command(command, url)
log_context["action"] = body
log_context["api_url"] = url
with get_logger().contextualize(**log_context):
await PRAgent().handle_request(url, body)
except Exception as e:
get_logger().error(f"Failed to handle command: {command} , error: {e}")
def _process_command(command: str, url) -> str:
# don't think we need this
apply_repo_settings(url)
# Process the command string
split_command = command.split(" ")
command = split_command[0]
args = split_command[1:]
# do I need this? if yes, shouldn't this be done in PRAgent?
other_args = update_settings_from_args(args)
new_command = ' '.join([command] + other_args)
return new_command
def _to_list(command_string: str) -> list:
try:
# Use ast.literal_eval to safely parse the string into a list
commands = ast.literal_eval(command_string)
# Check if the parsed object is a list of strings
if isinstance(commands, list) and all(isinstance(cmd, str) for cmd in commands):
return commands
else:
raise ValueError("Parsed data is not a list of strings.")
except (SyntaxError, ValueError, TypeError) as e:
raise ValueError(f"Invalid command string: {e}")
def _get_commands_list_from_settings(setting_key:str ) -> list:
try:
return get_settings().get(setting_key, [])
except ValueError as e:
get_logger().error(f"Failed to get commands list from settings {setting_key}: {e}")
@router.get("/") @router.get("/")
async def root(): async def root():
return {"status": "ok"} return {"status": "ok"}

View File

@ -37,7 +37,7 @@ async def run_action():
OPENAI_KEY = os.environ.get('OPENAI_KEY') or os.environ.get('OPENAI.KEY') OPENAI_KEY = os.environ.get('OPENAI_KEY') or os.environ.get('OPENAI.KEY')
OPENAI_ORG = os.environ.get('OPENAI_ORG') or os.environ.get('OPENAI.ORG') OPENAI_ORG = os.environ.get('OPENAI_ORG') or os.environ.get('OPENAI.ORG')
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) # get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
# Check if required environment variables are set # Check if required environment variables are set
if not GITHUB_EVENT_NAME: if not GITHUB_EVENT_NAME:
@ -80,10 +80,14 @@ async def run_action():
except Exception as e: except Exception as e:
get_logger().info(f"github action: failed to apply repo settings: {e}") get_logger().info(f"github action: failed to apply repo settings: {e}")
# Handle pull request event # Handle pull request opened 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", "ready_for_review", "review_requested"]:
# Retrieve the list of actions from the configuration
pr_actions = get_settings().get("GITHUB_ACTION_CONFIG.PR_ACTIONS", ["opened", "reopened", "ready_for_review", "review_requested"])
if action in pr_actions:
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 # legacy - supporting both GITHUB_ACTION and GITHUB_ACTION_CONFIG
@ -97,6 +101,11 @@ async def run_action():
if auto_improve is None: if auto_improve is None:
auto_improve = get_setting_or_env("GITHUB_ACTION_CONFIG.AUTO_IMPROVE", None) auto_improve = get_setting_or_env("GITHUB_ACTION_CONFIG.AUTO_IMPROVE", None)
# Set the configuration for auto actions
get_settings().config.is_auto_command = True # Set the flag to indicate that the command is auto
get_settings().pr_description.final_update_message = False # No final update message when auto_describe is enabled
get_logger().info(f"Running auto actions: auto_describe={auto_describe}, auto_review={auto_review}, auto_improve={auto_improve}")
# invoke by default all three tools # invoke by default all three tools
if auto_describe is None or is_true(auto_describe): if auto_describe is None or is_true(auto_describe):
await PRDescription(pr_url).run() await PRDescription(pr_url).run()
@ -126,7 +135,7 @@ async def run_action():
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 elif event_payload.get("comment", {}).get("pull_request_url"): # for 'pull_request_review_comment
url = event_payload.get("comment", {}).get("pull_request_url") url = event_payload.get("comment", {}).get("pull_request_url")
is_pr = True is_pr = True
disable_eyes = True disable_eyes = True
@ -138,8 +147,11 @@ async def run_action():
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, await PRAgent().handle_request(
notify=lambda: provider.add_eyes_reaction(comment_id, disable_eyes=disable_eyes)) 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

@ -7,6 +7,7 @@ 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
from starlette.background import BackgroundTasks
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette_context import context from starlette_context import context
from starlette_context.middleware import RawContextMiddleware from starlette_context.middleware import RawContextMiddleware
@ -14,7 +15,7 @@ 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.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, get_git_provider_with_context
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.git_providers.utils import apply_repo_settings
from pr_agent.identity_providers import get_identity_provider from pr_agent.identity_providers import get_identity_provider
@ -34,7 +35,7 @@ router = APIRouter()
@router.post("/api/v1/github_webhooks") @router.post("/api/v1/github_webhooks")
async def handle_github_webhooks(request: Request, response: Response): async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request, response: Response):
""" """
Receives and processes incoming GitHub webhook requests. Receives and processes incoming GitHub webhook requests.
Verifies the request signature, parses the request body, and passes it to the handle_request function for further Verifies the request signature, parses the request body, and passes it to the handle_request function for further
@ -47,9 +48,9 @@ async def handle_github_webhooks(request: Request, response: Response):
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)
context["git_provider"] = {}
response = await handle_request(body, event=request.headers.get("X-GitHub-Event", None)) background_tasks.add_task(handle_request, body, event=request.headers.get("X-GitHub-Event", None))
return response or {} return {}
@router.post("/api/v1/marketplace_webhooks") @router.post("/api/v1/marketplace_webhooks")
@ -110,7 +111,7 @@ async def handle_comments_on_pr(body: Dict[str, Any],
return {} return {}
log_context["api_url"] = api_url log_context["api_url"] = api_url
comment_id = body.get("comment", {}).get("id") comment_id = body.get("comment", {}).get("id")
provider = get_git_provider()(pr_url=api_url) provider = get_git_provider_with_context(pr_url=api_url)
with get_logger().contextualize(**log_context): with get_logger().contextualize(**log_context):
if get_identity_provider().verify_eligibility("github", sender_id, api_url) is not Eligibility.NOT_ELIGIBLE: 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}") get_logger().info(f"Processing comment on PR {api_url=}, comment_body={comment_body}")
@ -128,21 +129,15 @@ async def handle_new_pr_opened(body: Dict[str, Any],
agent: PRAgent): agent: PRAgent):
title = body.get("pull_request", {}).get("title", "") 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) pull_request, api_url = _check_pull_request_event(action, body, log_context)
if not (pull_request and api_url): if not (pull_request and api_url):
get_logger().info(f"Invalid PR event: {action=} {api_url=}") get_logger().info(f"Invalid PR event: {action=} {api_url=}")
return {} return {}
if action in get_settings().github_app.handle_pr_actions: # ['opened', 'reopened', 'ready_for_review'] if action in get_settings().github_app.handle_pr_actions: # ['opened', 'reopened', 'ready_for_review']
# logic to ignore PRs with specific titles (e.g. "[Auto] ...")
apply_repo_settings(api_url)
if get_identity_provider().verify_eligibility("github", sender_id, api_url) is not Eligibility.NOT_ELIGIBLE: 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) await _perform_auto_commands_github("pr_commands", agent, body, api_url, log_context)
else: else:
get_logger().info(f"User {sender=} is not eligible to process PR {api_url=}") get_logger().info(f"User {sender=} is not eligible to process PR {api_url=}")
@ -199,14 +194,9 @@ async def handle_push_trigger_for_new_commits(body: Dict[str, Any],
get_logger().info(f"Finished waiting to process push trigger for {api_url=} - continue with flow") get_logger().info(f"Finished waiting to process push trigger for {api_url=} - continue with flow")
try: 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: 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=}") 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) await _perform_auto_commands_github("push_commands", agent, body, api_url, log_context)
finally: finally:
# release the waiting task block # release the waiting task block
@ -240,13 +230,67 @@ def get_log_context(body, event, action, build_number):
app_name = get_settings().get("CONFIG.APP_NAME", "Unknown") app_name = get_settings().get("CONFIG.APP_NAME", "Unknown")
log_context = {"action": action, "event": event, "sender": sender, "server_type": "github_app", 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, "request_id": uuid.uuid4().hex, "build_number": build_number, "app_name": app_name,
"repo": repo, "git_org": git_org, "installation_id": installation_id} "repo": repo, "git_org": git_org, "installation_id": installation_id}
except Exception as e: except Exception as e:
get_logger().error("Failed to get log context", e) get_logger().error("Failed to get log context", e)
log_context = {} log_context = {}
return log_context, sender, sender_id, sender_type return log_context, sender, sender_id, sender_type
def is_bot_user(sender, sender_type):
try:
# logic to ignore PRs opened by bot
if get_settings().get("GITHUB_APP.IGNORE_BOT_PR", False) and sender_type == "Bot":
if 'pr-agent' not in sender:
get_logger().info(f"Ignoring PR from '{sender=}' because it is a bot")
return True
except Exception as e:
get_logger().error(f"Failed 'is_bot_user' logic: {e}")
return False
def should_process_pr_logic(sender_type, sender, body) -> bool:
try:
pull_request = body.get("pull_request", {})
title = pull_request.get("title", "")
pr_labels = pull_request.get("labels", [])
source_branch = pull_request.get("head", {}).get("ref", "")
target_branch = pull_request.get("base", {}).get("ref", "")
# logic to ignore PRs with specific titles
if title:
ignore_pr_title_re = get_settings().get("CONFIG.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 config.ignore_pr_title setting")
return False
# logic to ignore PRs with specific labels or source branches or target branches.
ignore_pr_labels = get_settings().get("CONFIG.IGNORE_PR_LABELS", [])
if pr_labels and ignore_pr_labels:
labels = [label['name'] for label in pr_labels]
if any(label in ignore_pr_labels for label in labels):
labels_str = ", ".join(labels)
get_logger().info(f"Ignoring PR with labels '{labels_str}' due to config.ignore_pr_labels settings")
return False
ignore_pr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
if pull_request and (ignore_pr_source_branches or ignore_pr_target_branches):
if any(re.search(regex, source_branch) for regex in ignore_pr_source_branches):
get_logger().info(
f"Ignoring PR with source branch '{source_branch}' due to config.ignore_pr_source_branches settings")
return False
if any(re.search(regex, target_branch) for regex in ignore_pr_target_branches):
get_logger().info(
f"Ignoring PR with target branch '{target_branch}' due to config.ignore_pr_target_branches settings")
return False
except Exception as e:
get_logger().error(f"Failed 'should_process_pr_logic': {e}")
return True
async def handle_request(body: Dict[str, Any], event: str): async def handle_request(body: Dict[str, Any], event: str):
""" """
Handle incoming GitHub webhook requests. Handle incoming GitHub webhook requests.
@ -255,30 +299,35 @@ async def handle_request(body: Dict[str, Any], event: str):
body: The request body. body: The request body.
event: The GitHub event type (e.g. "pull_request", "issue_comment", etc.). event: The GitHub event type (e.g. "pull_request", "issue_comment", etc.).
""" """
action = body.get("action") # "created", "opened", "reopened", "ready_for_review", "review_requested", "synchronize" 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()
log_context, sender, sender_id, sender_type = get_log_context(body, event, action, build_number) log_context, sender, sender_id, sender_type = get_log_context(body, event, action, build_number)
# logic to ignore PRs opened by bot # logic to ignore PRs opened by bot, PRs with specific titles, labels, source branches, or target branches
if get_settings().get("GITHUB_APP.IGNORE_BOT_PR", False) and sender_type == "Bot": if is_bot_user(sender, sender_type):
if 'pr-agent' not in sender:
get_logger().info(f"Ignoring PR from '{sender=}' because it is a bot")
return {} return {}
if action != 'created' and 'check_run' not in body:
if not should_process_pr_logic(sender_type, sender, body):
return {}
if 'check_run' in body: # handle failed checks
# get_logger().debug(f'Request body', artifact=body, event=event) # added inside handle_checks
pass
# handle comments on PRs # handle comments on PRs
if action == 'created': elif action == 'created':
get_logger().debug(f'Request body', artifact=body, event=event) get_logger().debug(f'Request body', artifact=body, event=event)
await handle_comments_on_pr(body, event, sender, sender_id, action, log_context, agent) await handle_comments_on_pr(body, event, sender, sender_id, action, log_context, agent)
# handle new PRs # handle new PRs
elif event == 'pull_request' and action != 'synchronize' and action != 'closed': elif event == 'pull_request' and action != 'synchronize' and action != 'closed':
get_logger().debug(f'Request body', artifact=body, event=event) get_logger().debug(f'Request body', artifact=body, event=event)
await handle_new_pr_opened(body, event, sender, sender_id, action, log_context, agent) await handle_new_pr_opened(body, event, sender, sender_id, action, log_context, agent)
elif event == "issue_comment" and 'edited' in action:
pass # handle_checkbox_clicked
# 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': elif event == 'pull_request' and action == 'synchronize':
get_logger().debug(f'Request body', artifact=body, event=event) await handle_push_trigger_for_new_commits(body, event, sender,sender_id, action, log_context, agent)
await handle_push_trigger_for_new_commits(body, event, sender, sender_id, action, log_context, agent)
elif event == 'pull_request' and action == 'closed': elif event == 'pull_request' and action == 'closed':
if get_settings().get("CONFIG.ANALYTICS_FOLDER", ""): if get_settings().get("CONFIG.ANALYTICS_FOLDER", ""):
handle_closed_pr(body, event, action, log_context) handle_closed_pr(body, event, action, log_context)
@ -321,22 +370,22 @@ def _check_pull_request_event(action: str, body: dict, log_context: dict) -> Tup
return pull_request, api_url return pull_request, api_url
async def _perform_auto_commands_github(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: if not commands:
with get_logger().contextualize(**log_context): get_logger().info(f"New PR, but no auto commands configured")
get_logger().info(f"New PR, but no auto commands configured")
return return
get_settings().set("config.is_auto_command", True)
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)
with get_logger().contextualize(**log_context): get_logger().info(f"{commands_conf}. Performing auto command '{new_command}', for {api_url=}")
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)
@router.get("/") @router.get("/")
@ -347,7 +396,7 @@ async def root():
if get_settings().github_app.override_deployment_type: if get_settings().github_app.override_deployment_type:
# Override the deployment type to app # Override the deployment type to app
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app") get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app")
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) # get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
middleware = [Middleware(RawContextMiddleware)] middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware) app = FastAPI(middleware=middleware)
app.include_router(router) app.include_router(router)
@ -356,5 +405,6 @@ app.include_router(router)
def start(): def start():
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

@ -1,6 +1,10 @@
import asyncio import asyncio
import multiprocessing
from collections import deque
import traceback
from datetime import datetime, timezone from datetime import datetime, timezone
import time
import requests
import aiohttp import aiohttp
from pr_agent.agent.pr_agent import PRAgent from pr_agent.agent.pr_agent import PRAgent
@ -12,10 +16,19 @@ setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
NOTIFICATION_URL = "https://api.github.com/notifications" NOTIFICATION_URL = "https://api.github.com/notifications"
async def mark_notification_as_read(headers, notification, session):
async with session.patch(
f"https://api.github.com/notifications/threads/{notification['id']}",
headers=headers) as mark_read_response:
if mark_read_response.status != 205:
get_logger().error(
f"Failed to mark notification as read. Status code: {mark_read_response.status}")
def now() -> str: def now() -> str:
""" """
Get the current UTC time in ISO 8601 format. Get the current UTC time in ISO 8601 format.
Returns: Returns:
str: The current UTC time in ISO 8601 format. str: The current UTC time in ISO 8601 format.
""" """
@ -23,6 +36,108 @@ def now() -> str:
now_utc = now_utc.replace("+00:00", "Z") now_utc = now_utc.replace("+00:00", "Z")
return now_utc return now_utc
async def async_handle_request(pr_url, rest_of_comment, comment_id, git_provider):
agent = PRAgent()
success = await agent.handle_request(
pr_url,
rest_of_comment,
notify=lambda: git_provider.add_eyes_reaction(comment_id)
)
return success
def run_handle_request(pr_url, rest_of_comment, comment_id, git_provider):
return asyncio.run(async_handle_request(pr_url, rest_of_comment, comment_id, git_provider))
def process_comment_sync(pr_url, rest_of_comment, comment_id):
try:
# Run the async handle_request in a separate function
git_provider = get_git_provider()(pr_url=pr_url)
success = run_handle_request(pr_url, rest_of_comment, comment_id, git_provider)
except Exception as e:
get_logger().error(f"Error processing comment: {e}", artifact={"traceback": traceback.format_exc()})
async def process_comment(pr_url, rest_of_comment, comment_id):
try:
git_provider = get_git_provider()(pr_url=pr_url)
git_provider.set_pr(pr_url)
agent = PRAgent()
success = await agent.handle_request(
pr_url,
rest_of_comment,
notify=lambda: git_provider.add_eyes_reaction(comment_id)
)
get_logger().info(f"Finished processing comment for PR: {pr_url}")
except Exception as e:
get_logger().error(f"Error processing comment: {e}", artifact={"traceback": traceback.format_exc()})
async def is_valid_notification(notification, headers, handled_ids, session, user_id):
try:
if 'reason' in notification and notification['reason'] == 'mention':
if 'subject' in notification and notification['subject']['type'] == 'PullRequest':
pr_url = notification['subject']['url']
latest_comment = notification['subject']['latest_comment_url']
if not latest_comment or not isinstance(latest_comment, str):
get_logger().debug(f"no latest_comment")
return False, handled_ids
async with session.get(latest_comment, headers=headers) as comment_response:
check_prev_comments = False
if comment_response.status == 200:
comment = await comment_response.json()
if 'id' in comment:
if comment['id'] in handled_ids:
get_logger().debug(f"comment['id'] in handled_ids")
return False, handled_ids
else:
handled_ids.add(comment['id'])
if 'user' in comment and 'login' in comment['user']:
if comment['user']['login'] == user_id:
get_logger().debug(f"comment['user']['login'] == user_id")
check_prev_comments = True
comment_body = comment.get('body', '')
if not comment_body:
get_logger().debug(f"no comment_body")
check_prev_comments = True
else:
user_tag = "@" + user_id
if user_tag not in comment_body:
get_logger().debug(f"user_tag not in comment_body")
check_prev_comments = True
else:
get_logger().info(f"Polling, pr_url: {pr_url}",
artifact={"comment": comment_body})
if not check_prev_comments:
return True, handled_ids, comment, comment_body, pr_url, user_tag
else: # we could not find the user tag in the latest comment. Check previous comments
# get all comments in the PR
requests_url = f"{pr_url}/comments".replace("pulls", "issues")
comments_response = requests.get(requests_url, headers=headers)
comments = comments_response.json()[::-1]
max_comment_to_scan = 4
for comment in comments[:max_comment_to_scan]:
if 'user' in comment and 'login' in comment['user']:
if comment['user']['login'] == user_id:
continue
comment_body = comment.get('body', '')
if not comment_body:
continue
if user_tag in comment_body:
get_logger().info("found user tag in previous comments")
get_logger().info(f"Polling, pr_url: {pr_url}",
artifact={"comment": comment_body})
return True, handled_ids, comment, comment_body, pr_url, user_tag
get_logger().error(f"Failed to fetch comments for PR: {pr_url}")
return False, handled_ids
return False, handled_ids
except Exception as e:
get_logger().error(f"Error processing notification: {e}", artifact={"traceback": traceback.format_exc()})
return False, handled_ids
async def polling_loop(): async def polling_loop():
""" """
@ -33,8 +148,8 @@ async def polling_loop():
last_modified = [None] last_modified = [None]
git_provider = get_git_provider()() git_provider = get_git_provider()()
user_id = git_provider.get_user_id() user_id = git_provider.get_user_id()
agent = PRAgent()
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False) get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
get_settings().set("pr_description.publish_description_as_comment", True)
try: try:
deployment_type = get_settings().github.deployment_type deployment_type = get_settings().github.deployment_type
@ -72,44 +187,53 @@ async def polling_loop():
notifications = await response.json() notifications = await response.json()
if not notifications: if not notifications:
continue continue
get_logger().info(f"Received {len(notifications)} notifications")
task_queue = deque()
for notification in notifications: for notification in notifications:
if not notification:
continue
# mark notification as read
await mark_notification_as_read(headers, notification, session)
handled_ids.add(notification['id']) handled_ids.add(notification['id'])
if 'reason' in notification and notification['reason'] == 'mention': output = await is_valid_notification(notification, headers, handled_ids, session, user_id)
if 'subject' in notification and notification['subject']['type'] == 'PullRequest': if output[0]:
pr_url = notification['subject']['url'] _, handled_ids, comment, comment_body, pr_url, user_tag = output
latest_comment = notification['subject']['latest_comment_url'] rest_of_comment = comment_body.split(user_tag)[1].strip()
async with session.get(latest_comment, headers=headers) as comment_response: comment_id = comment['id']
if comment_response.status == 200:
comment = await comment_response.json() # Add to the task queue
if 'id' in comment: get_logger().info(
if comment['id'] in handled_ids: f"Adding comment processing to task queue for PR, {pr_url}, comment_body: {comment_body}")
continue task_queue.append((process_comment_sync, (pr_url, rest_of_comment, comment_id)))
else: get_logger().info(f"Queued comment processing for PR: {pr_url}")
handled_ids.add(comment['id']) else:
if 'user' in comment and 'login' in comment['user']: get_logger().debug(f"Skipping comment processing for PR")
if comment['user']['login'] == user_id:
continue max_allowed_parallel_tasks = 10
comment_body = comment['body'] if 'body' in comment else '' if task_queue:
commenter_github_user = comment['user']['login'] \ processes = []
if 'user' in comment else '' for i, (func, args) in enumerate(task_queue): # Create parallel tasks
get_logger().info(f"Commenter: {commenter_github_user}\nComment: {comment_body}") p = multiprocessing.Process(target=func, args=args)
user_tag = "@" + user_id processes.append(p)
if user_tag not in comment_body: p.start()
continue if i > max_allowed_parallel_tasks:
rest_of_comment = comment_body.split(user_tag)[1].strip() get_logger().error(
comment_id = comment['id'] f"Dropping {len(task_queue) - max_allowed_parallel_tasks} tasks from polling session")
git_provider.set_pr(pr_url) break
success = await agent.handle_request(pr_url, rest_of_comment, task_queue.clear()
notify=lambda: git_provider.add_eyes_reaction(comment_id)) # noqa E501
if not success: # Dont wait for all processes to complete. Move on to the next iteration
git_provider.set_pr(pr_url) # for p in processes:
# p.join()
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}")
except Exception as e: except Exception as e:
get_logger().error(f"Exception during processing of a notification: {e}") get_logger().error(f"Polling exception during processing of a notification: {e}",
artifact={"traceback": traceback.format_exc()})
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(polling_loop()) asyncio.run(polling_loop())

View File

@ -1,5 +1,7 @@
import copy import copy
import re
import json import json
from datetime import datetime
import uvicorn import uvicorn
from fastapi import APIRouter, FastAPI, Request, status from fastapi import APIRouter, FastAPI, Request, status
@ -23,17 +25,44 @@ 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
def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_context: dict): async def get_mr_url_from_commit_sha(commit_sha, gitlab_token, project_id):
try:
import requests
headers = {
'Private-Token': f'{gitlab_token}'
}
# API endpoint to find MRs containing the commit
gitlab_url = get_settings().get("GITLAB.URL", 'https://gitlab.com')
response = requests.get(
f'{gitlab_url}/api/v4/projects/{project_id}/repository/commits/{commit_sha}/merge_requests',
headers=headers
)
merge_requests = response.json()
if merge_requests and response.status_code == 200:
pr_url = merge_requests[0]['web_url']
return pr_url
else:
get_logger().info(f"No merge requests found for commit: {commit_sha}")
return None
except Exception as e:
get_logger().error(f"Failed to get MR url from commit sha: {e}")
return None
async def handle_request(api_url: str, body: str, log_context: dict, sender_id: str):
log_context["action"] = body log_context["action"] = body
log_context["event"] = "pull_request" if body == "/review" else "comment" log_context["event"] = "pull_request" if body == "/review" else "comment"
log_context["api_url"] = url log_context["api_url"] = api_url
log_context["app_name"] = get_settings().get("CONFIG.APP_NAME", "Unknown")
with get_logger().contextualize(**log_context): with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body) await PRAgent().handle_request(api_url, body)
async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict): async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url: str,
log_context: dict):
apply_repo_settings(api_url) apply_repo_settings(api_url)
commands = get_settings().get(f"gitlab.{commands_conf}", {}) commands = get_settings().get(f"gitlab.{commands_conf}", {})
get_settings().set("config.is_auto_command", True)
for command in commands: for command in commands:
try: try:
split_command = command.split(" ") split_command = command.split(" ")
@ -48,55 +77,156 @@ async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url:
get_logger().error(f"Failed to perform command {command}: {e}") get_logger().error(f"Failed to perform command {command}: {e}")
def is_bot_user(data) -> bool:
try:
# logic to ignore bot users (unlike Github, no direct flag for bot users in gitlab)
sender_name = data.get("user", {}).get("name", "unknown").lower()
bot_indicators = ['codium', 'bot_', 'bot-', '_bot', '-bot']
if any(indicator in sender_name for indicator in bot_indicators):
get_logger().info(f"Skipping GitLab bot user: {sender_name}")
return True
except Exception as e:
get_logger().error(f"Failed 'is_bot_user' logic: {e}")
return False
def should_process_pr_logic(data, title) -> bool:
try:
# logic to ignore MRs for titles, labels and source, target branches.
ignore_mr_title = get_settings().get("CONFIG.IGNORE_PR_TITLE", [])
ignore_mr_labels = get_settings().get("CONFIG.IGNORE_PR_LABELS", [])
ignore_mr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
ignore_mr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
#
if ignore_mr_source_branches:
source_branch = data['object_attributes'].get('source_branch')
if any(re.search(regex, source_branch) for regex in ignore_mr_source_branches):
get_logger().info(
f"Ignoring MR with source branch '{source_branch}' due to gitlab.ignore_mr_source_branches settings")
return False
if ignore_mr_target_branches:
target_branch = data['object_attributes'].get('target_branch')
if any(re.search(regex, target_branch) for regex in ignore_mr_target_branches):
get_logger().info(
f"Ignoring MR with target branch '{target_branch}' due to gitlab.ignore_mr_target_branches settings")
return False
if ignore_mr_labels:
labels = [label['title'] for label in data['object_attributes'].get('labels', [])]
if any(label in ignore_mr_labels for label in labels):
labels_str = ", ".join(labels)
get_logger().info(f"Ignoring MR with labels '{labels_str}' due to gitlab.ignore_mr_labels settings")
return False
if ignore_mr_title:
if any(re.search(regex, title) for regex in ignore_mr_title):
get_logger().info(f"Ignoring MR with title '{title}' due to gitlab.ignore_mr_title settings")
return False
except Exception as e:
get_logger().error(f"Failed 'should_process_pr_logic': {e}")
return True
@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"} start_time = datetime.now()
get_logger().debug("Received a GitLab webhook") request_json = await request.json()
# Check if the request is authorized async def inner(data: dict):
if request.headers.get("X-Gitlab-Token") and secret_provider: log_context = {"server_type": "gitlab_app"}
request_token = request.headers.get("X-Gitlab-Token") get_logger().debug("Received a GitLab webhook")
secret = secret_provider.get_secret(request_token) if request.headers.get("X-Gitlab-Token") and secret_provider:
try: request_token = request.headers.get("X-Gitlab-Token")
secret_dict = json.loads(secret) secret = secret_provider.get_secret(request_token)
gitlab_token = secret_dict["gitlab_token"] if not secret:
log_context["sender"] = secret_dict.get("token_name", secret_dict.get("id", "unknown")) get_logger().warning(f"Empty secret retrieved, request_token: {request_token}")
context["settings"] = copy.deepcopy(global_settings) return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED,
context["settings"].gitlab.personal_access_token = gitlab_token content=jsonable_encoder({"message": "unauthorized"}))
except Exception as e: try:
get_logger().error(f"Failed to validate secret {request_token}: {e}") secret_dict = json.loads(secret)
gitlab_token = secret_dict["gitlab_token"]
log_context["token_id"] = secret_dict.get("token_name", secret_dict.get("id", "unknown"))
context["settings"] = copy.deepcopy(global_settings)
context["settings"].gitlab.personal_access_token = gitlab_token
except Exception as e:
get_logger().error(f"Failed to validate secret {request_token}: {e}")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
elif get_settings().get("GITLAB.SHARED_SECRET"):
secret = get_settings().get("GITLAB.SHARED_SECRET")
if not request.headers.get("X-Gitlab-Token") == secret:
get_logger().error("Failed to validate secret")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
else:
get_logger().error("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"}))
elif get_settings().get("GITLAB.SHARED_SECRET"): gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
secret = get_settings().get("GITLAB.SHARED_SECRET") if not gitlab_token:
if not request.headers.get("X-Gitlab-Token") == secret: get_logger().error("No gitlab token found")
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:
get_logger().error(f"Failed to validate secret")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
if not gitlab_token:
get_logger().error(f"No gitlab token found")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
data = await request.json() get_logger().info("GitLab data", artifact=data)
get_logger().info("GitLab data", artifact=data) sender = data.get("user", {}).get("username", "unknown")
sender_id = data.get("user", {}).get("id", "unknown")
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']: # ignore bot users
url = data['object_attributes'].get('url') if is_bot_user(data):
get_logger().info(f"New merge request: {url}") return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context) if data.get('event_type') != 'note' and data.get('object_attributes', {}): # not a comment
elif data.get('object_kind') == 'note' and data['event_type'] == 'note': # comment on MR # ignore MRs based on title, labels, source and target branches
if 'merge_request' in data: if not should_process_pr_logic(data, data['object_attributes'].get('title')):
mr = data['merge_request'] return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
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')
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) log_context["sender"] = sender
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
title = data['object_attributes'].get('title')
url = data['object_attributes'].get('url')
draft = data['object_attributes'].get('draft')
get_logger().info(f"New merge request: {url}")
if draft:
get_logger().info(f"Skipping draft MR: {url}")
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context)
elif data.get('object_kind') == 'note' and data.get('event_type') == 'note': # comment on MR
if 'merge_request' in data:
mr = data['merge_request']
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')
if data.get('object_attributes', {}).get('type') == 'DiffNote' and '/ask' in body: # /ask_line
body = handle_ask_line(body, data)
await handle_request(url, body, log_context, sender_id)
elif data.get('object_kind') == 'push' and data.get('event_name') == 'push':
try:
project_id = data['project_id']
commit_sha = data['checkout_sha']
url = await get_mr_url_from_commit_sha(commit_sha, gitlab_token, project_id)
if not url:
get_logger().info(f"No MR found for commit: {commit_sha}")
return JSONResponse(status_code=status.HTTP_200_OK,
content=jsonable_encoder({"message": "success"}))
# we need first to apply_repo_settings
apply_repo_settings(url)
commands_on_push = get_settings().get(f"gitlab.push_commands", {})
handle_push_trigger = get_settings().get(f"gitlab.handle_push_trigger", False)
if not commands_on_push or not handle_push_trigger:
get_logger().info("Push event, but no push commands found or push trigger is disabled")
return JSONResponse(status_code=status.HTTP_200_OK,
content=jsonable_encoder({"message": "success"}))
get_logger().debug(f'A push event has been received: {url}')
await _perform_commands_gitlab("push_commands", PRAgent(), url, log_context)
except Exception as e:
get_logger().error(f"Failed to handle push event: {e}")
background_tasks.add_task(inner, request_json)
end_time = datetime.now()
get_logger().info(f"Processing time: {end_time - start_time}", request=request_json)
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"}))
@ -113,7 +243,7 @@ def handle_ask_line(body, data):
path = data['object_attributes']['position']['new_path'] path = data['object_attributes']['position']['new_path']
side = 'RIGHT' # if line_range_['start']['type'] == 'new' else 'LEFT' side = 'RIGHT' # if line_range_['start']['type'] == 'new' else 'LEFT'
comment_id = data['object_attributes']["discussion_id"] comment_id = data['object_attributes']["discussion_id"]
get_logger().info(f"Handling line comment") get_logger().info("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}" 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: except Exception as e:
get_logger().error(f"Failed to handle ask line comment: {e}") get_logger().error(f"Failed to handle ask line comment: {e}")
@ -124,15 +254,16 @@ def handle_ask_line(body, data):
async def root(): async def root():
return {"status": "ok"} return {"status": "ok"}
def start(): gitlab_url = get_settings().get("GITLAB.URL", None)
gitlab_url = get_settings().get("GITLAB.URL", None) if not gitlab_url:
if not gitlab_url: raise ValueError("GITLAB.URL is not set")
raise ValueError("GITLAB.URL is not set") get_settings().config.git_provider = "gitlab"
get_settings().config.git_provider = "gitlab" middleware = [Middleware(RawContextMiddleware)]
middleware = [Middleware(RawContextMiddleware)] app = FastAPI(middleware=middleware)
app = FastAPI(middleware=middleware) app.include_router(router)
app.include_router(router)
def start():
uvicorn.run(app, host="0.0.0.0", port=3000) uvicorn.run(app, host="0.0.0.0", port=3000)

View File

@ -0,0 +1,191 @@
import multiprocessing
import os
# from prometheus_client import multiprocess
# Sample Gunicorn configuration file.
#
# Server socket
#
# bind - The socket to bind.
#
# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
# An IP is a valid HOST.
#
# backlog - The number of pending connections. This refers
# to the number of clients that can be waiting to be
# served. Exceeding this number results in the client
# getting an error when attempting to connect. It should
# only affect servers under significant load.
#
# Must be a positive integer. Generally set in the 64-2048
# range.
#
# bind = '0.0.0.0:5000'
bind = '0.0.0.0:3000'
backlog = 2048
#
# Worker processes
#
# workers - The number of worker processes that this server
# should keep alive for handling requests.
#
# A positive integer generally in the 2-4 x $(NUM_CORES)
# range. You'll want to vary this a bit to find the best
# for your particular application's work load.
#
# worker_class - The type of workers to use. The default
# sync class should handle most 'normal' types of work
# loads. You'll want to read
# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
# for information on when you might want to choose one
# of the other worker classes.
#
# A string referring to a Python path to a subclass of
# gunicorn.workers.base.Worker. The default provided values
# can be seen at
# http://docs.gunicorn.org/en/latest/settings.html#worker-class
#
# worker_connections - For the eventlet and gevent worker classes
# this limits the maximum number of simultaneous clients that
# a single process can handle.
#
# A positive integer generally set to around 1000.
#
# timeout - If a worker does not notify the master process in this
# number of seconds it is killed and a new worker is spawned
# to replace it.
#
# Generally set to thirty seconds. Only set this noticeably
# higher if you're sure of the repercussions for sync workers.
# For the non sync workers it just means that the worker
# process is still communicating and is not tied to the length
# of time required to handle a single request.
#
# keepalive - The number of seconds to wait for the next request
# on a Keep-Alive HTTP connection.
#
# A positive integer. Generally set in the 1-5 seconds range.
#
if os.getenv('GUNICORN_WORKERS', None):
workers = int(os.getenv('GUNICORN_WORKERS'))
else:
cores = multiprocessing.cpu_count()
workers = cores * 2 + 1
worker_connections = 1000
timeout = 240
keepalive = 2
#
# spew - Install a trace function that spews every line of Python
# that is executed when running the server. This is the
# nuclear option.
#
# True or False
#
spew = False
#
# Server mechanics
#
# daemon - Detach the main Gunicorn process from the controlling
# terminal with a standard fork/fork sequence.
#
# True or False
#
# raw_env - Pass environment variables to the execution environment.
#
# pidfile - The path to a pid file to write
#
# A path string or None to not write a pid file.
#
# user - Switch worker processes to run as this user.
#
# A valid user id (as an integer) or the name of a user that
# can be retrieved with a call to pwd.getpwnam(value) or None
# to not change the worker process user.
#
# group - Switch worker process to run as this group.
#
# A valid group id (as an integer) or the name of a user that
# can be retrieved with a call to pwd.getgrnam(value) or None
# to change the worker processes group.
#
# umask - A mask for file permissions written by Gunicorn. Note that
# this affects unix socket permissions.
#
# A valid value for the os.umask(mode) call or a string
# compatible with int(value, 0) (0 means Python guesses
# the base, so values like "0", "0xFF", "0022" are valid
# for decimal, hex, and octal representations)
#
# tmp_upload_dir - A directory to store temporary request data when
# requests are read. This will most likely be disappearing soon.
#
# A path to a directory where the process owner can write. Or
# None to signal that Python should choose one on its own.
#
daemon = False
raw_env = []
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
#
# Logging
#
# logfile - The path to a log file to write to.
#
# A path string. "-" means log to stdout.
#
# loglevel - The granularity of log output
#
# A string of "debug", "info", "warning", "error", "critical"
#
errorlog = '-'
loglevel = 'info'
accesslog = None
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
#
# Process naming
#
# proc_name - A base to use with setproctitle to change the way
# that Gunicorn processes are reported in the system process
# table. This affects things like 'ps' and 'top'. If you're
# going to be running more than one instance of Gunicorn you'll
# probably want to set a name to tell them apart. This requires
# that you install the setproctitle module.
#
# A string or None to choose a default of something like 'gunicorn'.
#
proc_name = None
#
# Server hooks
#
# post_fork - Called just after a worker has been forked.
#
# A callable that takes a server and worker instance
# as arguments.
#
# pre_fork - Called just prior to forking the worker subprocess.
#
# A callable that accepts the same arguments as after_fork
#
# pre_exec - Called just prior to forking off a secondary
# master process during things like config reloading.
#
# A callable that takes a server instance as the sole argument.
#

View File

@ -34,7 +34,7 @@ key = "" # Acquire through https://console.groq.com/keys
[huggingface] [huggingface]
key = "" # Optional, uncomment if you want to use Huggingface Inference API. Acquire through https://huggingface.co/docs/api-inference/quicktour key = "" # Optional, uncomment if you want to use Huggingface Inference API. Acquire through https://huggingface.co/docs/api-inference/quicktour
api_base = "" # the base url for your huggingface inference endpoint api_base = "" # the base url for your huggingface inference endpoint
[ollama] [ollama]
api_base = "" # the base url for your local Llama 2, Code Llama, and other models inference endpoint. Acquire through https://ollama.ai/ api_base = "" # the base url for your local Llama 2, Code Llama, and other models inference endpoint. Acquire through https://ollama.ai/
@ -43,13 +43,10 @@ api_base = "" # the base url for your local Llama 2, Code Llama, and other model
vertex_project = "" # the google cloud platform project name for your vertexai deployment vertex_project = "" # the google cloud platform project name for your vertexai deployment
vertex_location = "" # the google cloud platform location for your vertexai deployment vertex_location = "" # the google cloud platform location for your vertexai deployment
[aws]
bedrock_region = "" # the AWS region to call Bedrock APIs
[github] [github]
# ---- Set the following only for deployment type == "user" # ---- Set the following only for deployment type == "user"
user_token = "" # A GitHub personal access token with 'repo' scope. user_token = "" # A GitHub personal access token with 'repo' scope.
deployment_type = "user" #set to user by default deployment_type = "user" #set to user by default
# ---- Set the following only for deployment type == "app", see README for details. # ---- Set the following only for deployment type == "app", see README for details.
private_key = """\ private_key = """\
@ -70,7 +67,7 @@ bearer_token = ""
[bitbucket_server] [bitbucket_server]
# For Bitbucket Server bearer token # For Bitbucket Server bearer token
auth_token = "" bearer_token = ""
webhook_secret = "" webhook_secret = ""
# For Bitbucket app # For Bitbucket app

View File

@ -1,25 +1,48 @@
[config] [config]
# models
model="gpt-4-turbo-2024-04-09" model="gpt-4-turbo-2024-04-09"
model_turbo="gpt-4o" model_turbo="gpt-4o-2024-08-06"
fallback_models=["gpt-4-0125-preview"] fallback_models=["gpt-4o-2024-05-13"]
# CLI
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
# Configurations
use_wiki_settings_file=true use_wiki_settings_file=true
use_repo_settings_file=true use_repo_settings_file=true
use_global_settings_file=true use_global_settings_file=true
ai_timeout=120 # 2minutes ai_timeout=120 # 2minutes
skip_keys = []
# token limits
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 = 1 custom_model_max_tokens=-1 # for models not in the default list
secret_provider="google_cloud_storage" # patch extension logic
patch_extension_skip_types =[".md",".txt"]
allow_dynamic_context=true
max_extra_lines_before_dynamic_context = 8 # will try to include up to 10 extra lines before the hunk in the patch, until we reach an enclosing function or class
patch_extra_lines_before = 3 # Number of extra lines (+3 default ones) to include before each hunk in the patch
patch_extra_lines_after = 1 # Number of extra lines (+3 default ones) to include after each hunk in the patch
secret_provider=""
cli_mode=false cli_mode=false
ai_disclaimer_title="" # Pro feature, title for a collapsible disclaimer to AI outputs ai_disclaimer_title="" # Pro feature, title for a collapsible disclaimer to AI outputs
ai_disclaimer="" # Pro feature, full text for the AI disclaimer ai_disclaimer="" # Pro feature, full text for the AI disclaimer
output_relevant_configurations=false output_relevant_configurations=false
large_patch_policy = "clip" # "clip", "skip"
# seed
seed=-1 # set positive value to fix the seed (and ensure temperature=0)
temperature=0.2
# ignore logic
ignore_pr_title = ["^\\[Auto\\]", "^Auto"] # a list of regular expressions to match against the PR title to ignore the PR agent
ignore_pr_target_branches = [] # a list of regular expressions of target branches to ignore from PR agent when an PR is created
ignore_pr_source_branches = [] # a list of regular expressions of source branches to ignore from PR agent when an PR is created
ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created
#
is_auto_command = false # will be auto-set to true if the command is triggered by an automation
enable_ai_metadata = false # will enable adding ai metadata
[pr_reviewer] # /review # [pr_reviewer] # /review #
# enable/disable features # enable/disable features
@ -27,11 +50,12 @@ require_score_review=false
require_tests_review=true require_tests_review=true
require_estimate_effort_to_review=true require_estimate_effort_to_review=true
require_can_be_split_review=false require_can_be_split_review=false
require_security_review=true
# soc2 # soc2
require_soc2_ticket=false 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.) ?" 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=0
inline_code_comments = false inline_code_comments = false
ask_and_reflect=false ask_and_reflect=false
#automatic_review=true #automatic_review=true
@ -45,6 +69,7 @@ enable_review_labels_effort=true
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_intro_text=true
enable_help_text=false # Determines whether to include help text in the PR review. Enabled by default. enable_help_text=false # Determines whether to include help text in the PR review. Enabled by default.
# auto approval # auto approval
enable_auto_approval=false enable_auto_approval=false
@ -71,7 +96,11 @@ 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
# large pr mode 💎
enable_large_pr_handling=true
max_ai_calls=4
async_ai_calls=true
mention_extra_files=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 #
@ -79,16 +108,23 @@ enable_help_text=false
[pr_code_suggestions] # /improve # [pr_code_suggestions] # /improve #
max_context_tokens=8000 max_context_tokens=14000
num_code_suggestions=4 #
commitable_code_suggestions = false commitable_code_suggestions = false
dual_publishing_score_threshold=-1 # -1 to disable, [0-10] to set the threshold (>=) for publishing a code suggestion both in a table and as commitable
#
extra_instructions = "" extra_instructions = ""
rank_suggestions = false rank_suggestions = false
enable_help_text=false enable_help_text=false
persistent_comment=false enable_chat_text=false
enable_intro_text=true
persistent_comment=true
max_history_len=4
# enable to apply suggestion 💎
apply_suggestions_checkbox=true
# suggestions scoring # suggestions scoring
self_reflect_on_suggestions=true self_reflect_on_suggestions=true
suggestions_score_threshold=0 # [0-10]. highly recommend not to set this value above 8, since above it may clip highly relevant suggestions suggestions_score_threshold=0 # [0-10]| recommend not to set this value above 8, since above it may clip highly relevant suggestions
# params for '/improve --extended' mode # params for '/improve --extended' mode
auto_extended_mode=true auto_extended_mode=true
num_code_suggestions_per_chunk=4 num_code_suggestions_per_chunk=4
@ -96,16 +132,38 @@ max_number_of_calls = 3
parallel_calls = true parallel_calls = true
rank_extended_suggestions = false rank_extended_suggestions = false
final_clip_factor = 0.8 final_clip_factor = 0.8
# self-review checkbox
demand_code_suggestions_self_review=false # add a checkbox for the author to self-review the code suggestions
code_suggestions_self_review_text= "**Author self-review**: I have reviewed the PR code suggestions, and addressed the relevant ones."
approve_pr_on_self_review=false # Pro feature. if true, the PR will be auto-approved after the author clicks on the self-review checkbox
# Suggestion impact
publish_post_process_suggestion_impact=true
[pr_custom_prompt] # /custom_prompt #
prompt = """\
The code suggestions should focus only on the following:
- ...
- ...
...
"""
suggestions_score_threshold=0
num_code_suggestions_per_chunk=4
self_reflect_on_custom_suggestions=true
enable_help_text=false
[pr_add_docs] # /add_docs # [pr_add_docs] # /add_docs #
extra_instructions = "" extra_instructions = ""
docs_style = "Sphinx Style" # "Google Style with Args, Returns, Attributes...etc", "Numpy Style", "Sphinx Style", "PEP257", "reStructuredText" docs_style = "Sphinx" # "Google Style with Args, Returns, Attributes...etc", "Numpy Style", "Sphinx Style", "PEP257", "reStructuredText"
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
[pr_update_changelog] # /update_changelog # [pr_update_changelog] # /update_changelog #
push_changelog_changes=false push_changelog_changes=false
extra_instructions = "" extra_instructions = ""
[pr_analyze] # /analyze # [pr_analyze] # /analyze #
enable_help_text=true
[pr_test] # /test # [pr_test] # /test #
extra_instructions = "" extra_instructions = ""
@ -120,15 +178,18 @@ enable_help_text=false
num_code_suggestions=4 num_code_suggestions=4
extra_instructions = "" extra_instructions = ""
file = "" # in case there are several components with the same name, you can specify the relevant file file = "" # in case there are several components with the same name, you can specify the relevant file
class_name = "" class_name = "" # in case there are several methods with the same name in the same file, you can specify the relevant class name
[checks] # /checks (pro feature) # [checks] # /checks (pro feature) #
enable_auto_checks_feedback=true enable_auto_checks_feedback=true
excluded_checks_list=["lint"] # list of checks to exclude, for example: ["check1", "check2"] excluded_checks_list=["lint"] # list of checks to exclude, for example: ["check1", "check2"]
persistent_comment=true persistent_comment=true
enable_help_text=true enable_help_text=true
final_update_message = false
[pr_help] # /help # [pr_help] # /help #
force_local_db=false
num_retrieved_snippets=5
[pr_config] # /config # [pr_config] # /config #
@ -139,20 +200,23 @@ ratelimit_retries = 5
base_url = "https://api.github.com" base_url = "https://api.github.com"
publish_inline_comments_fallback_with_verification = true publish_inline_comments_fallback_with_verification = true
try_fix_invalid_inline_comments = true try_fix_invalid_inline_comments = true
app_name = "pr-agent"
ignore_bot_pr = true
[github_action_config] [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
# enable_output = true # set as env var in .github/workflows/pr-agent.yaml # pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
[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
# settings for "pull_request" event # settings for "pull_request" event
handle_pr_actions = ['opened', 'reopened', 'ready_for_review'] handle_pr_actions = ['opened', 'reopened', 'ready_for_review']
pr_commands = [ pr_commands = [
"/describe", "/describe --pr_description.final_update_message=false",
"/review --pr_reviewer.num_code_suggestions=0", "/review --pr_reviewer.num_code_suggestions=0",
"/improve", "/improve",
] ]
@ -167,23 +231,27 @@ push_commands = [
"/describe", "/describe",
"/review --pr_reviewer.num_code_suggestions=0", "/review --pr_reviewer.num_code_suggestions=0",
] ]
ignore_pr_title = []
ignore_bot_pr = true
[gitlab] [gitlab]
url = "https://gitlab.com" # URL to the gitlab service url = "https://gitlab.com"
pr_commands = [ pr_commands = [
"/describe", "/describe --pr_description.final_update_message=false",
"/review --pr_reviewer.num_code_suggestions=0", "/review --pr_reviewer.num_code_suggestions=0",
"/improve", "/improve",
] ]
handle_push_trigger = false
push_commands = [
"/describe",
"/review --pr_reviewer.num_code_suggestions=0",
]
[bitbucket_app] [bitbucket_app]
pr_commands = [ pr_commands = [
"/describe --pr_description.final_update_message=false",
"/review --pr_reviewer.num_code_suggestions=0", "/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.commitable_code_suggestions=true", "/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
] ]
avoid_full_files = false
[local] [local]
# LocalGitProvider settings - uncomment to use paths other than default # LocalGitProvider settings - uncomment to use paths other than default
@ -204,10 +272,19 @@ pr_commands = [
# URL to the BitBucket Server instance # URL to the BitBucket Server instance
# url = "https://git.bitbucket.com" # url = "https://git.bitbucket.com"
url = "" url = ""
pr_commands = [
"/describe --pr_description.final_update_message=false",
"/review --pr_reviewer.num_code_suggestions=0",
"/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
]
[litellm] [litellm]
# use_client = false # use_client = false
# drop_params = false # drop_params = false
enable_callbacks = false
success_callback = []
failure_callback = []
service_callback = []
[pr_similar_issue] [pr_similar_issue]
skip_comments = false skip_comments = false
@ -215,10 +292,22 @@ force_update_dataset = false
max_issues_to_scan = 500 max_issues_to_scan = 500
vectordb = "pinecone" vectordb = "pinecone"
[pr_find_similar_component]
class_name = ""
file = ""
search_from_org = false
allow_fallback_less_words = true
number_of_keywords = 5
number_of_results = 5
[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] [lancedb]
uri = "./lancedb" uri = "./lancedb"
[best_practices]
content = ""
max_lines_allowed = 800
enable_global_best_practices = false

View File

@ -8,4 +8,5 @@ glob = [
regex = [ regex = [
# Ignore files and directories matching these regex patterns. # Ignore files and directories matching these regex patterns.
# See https://learnbyexample.github.io/python-regex-cheatsheet/ # See https://learnbyexample.github.io/python-regex-cheatsheet/
# for example: regex = ['.*\.toml$']
] ]

View File

@ -44,6 +44,7 @@ default = [
'ss', 'ss',
'svg', 'svg',
'tar', 'tar',
'tgz',
'tsv', 'tsv',
'ttf', 'ttf',
'war', 'war',
@ -62,6 +63,7 @@ extra = [
] ]
[language_extension_map_org] [language_extension_map_org]
"1C Enterprise" = ["*.bsl", ]
ABAP = [".abap", ] ABAP = [".abap", ]
"AGS Script" = [".ash", ] "AGS Script" = [".ash", ]
AMPL = [".ampl", ] AMPL = [".ampl", ]

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:
====== ======
## file: 'src/file1.py' ## File: 'src/file1.py'
@@ -12,3 +12,4 @@ def func1(): @@ -12,3 +12,4 @@ def func1():
__new hunk__ __new hunk__
@ -25,7 +25,7 @@ __old hunk__
... ...
## file: 'src/file2.py' ## File: 'src/file2.py'
... ...
====== ======
@ -56,7 +56,7 @@ Code Documentation:
items: items:
relevant file: relevant file:
type: string type: string
description: the relevant file full path description: The full file path of the relevant file.
relevant line: relevant line:
type: integer type: integer
description: |- description: |-

View File

@ -1,21 +1,28 @@
[pr_code_suggestions_prompt] [pr_code_suggestions_prompt]
system="""You are PR-Reviewer, a language model that specializes in suggesting ways to improve for a Pull Request (PR) code. system="""You are PR-Reviewer, an AI specializing in Pull Request (PR) code analysis and suggestions.
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR diff. Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix possible bugs and problems, and enhance code quality, readability, and performance.
The format we will use to present the PR code diff: The PR code diff will be in the following structured format:
====== ======
## file: 'src/file1.py' ## File: 'src/file1.py'
{%- if is_ai_metadata %}
### AI-generated changes summary:
* ...
* ...
{%- endif %}
@@ ... @@ def func1(): @@ ... @@ def func1():
__new hunk__ __new hunk__
12 code line1 that remained unchanged in the PR 11 unchanged code line0 in the PR
13 +new hunk code line2 added in the PR 12 unchanged code line1 in the PR
14 code line3 that remained unchanged in the PR 13 +new code line2 added in the PR
14 unchanged code line3 in the PR
__old hunk__ __old hunk__
code line1 that remained unchanged in the PR unchanged code line0
-old hunk code line2 that was removed in the PR unchanged code line1
code line3 that remained unchanged in the PR -old code line2 removed in the PR
unchanged code line3
@@ ... @@ def func2(): @@ ... @@ def func2():
__new hunk__ __new hunk__
@ -24,29 +31,31 @@ __old hunk__
... ...
## file: 'src/file2.py' ## File: 'src/file2.py'
... ...
====== ======
- In this format, we separated each hunk of code to '__new hunk__' and '__old hunk__' sections. The '__new hunk__' section contains the new code of the chunk, and the '__old hunk__' section contains the old code that was removed.
- Code lines are prefixed symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. - In the format above, the diff is organized into separate '__new hunk__' and '__old hunk__' sections for each code chunk. '__new hunk__' contains the updated code, while '__old hunk__' shows the removed code. If no code was added or removed in a specific chunk, the corresponding section will be omitted.
- We also added line numbers for the '__new hunk__' sections, to help you refer to the code lines in your suggestions. These line numbers are not part of the actual code, and are only used for reference. - Line numbers were added for the '__new hunk__' sections to help referencing specific lines in the code suggestions. These numbers are for reference only and are not part of the actual code.
- Code lines are prefixed with symbols: '+' for new code added in the PR, '-' for code removed, and ' ' for unchanged code.
{%- if is_ai_metadata %}
- When available, an AI-generated summary will precede each file's diff, with a high-level overview of the changes. Note that this summary may not be fully accurate or complete.
{%- endif %}
Specific instructions for generating code suggestions: Specific guidelines for generating code suggestions:
- Provide up to {{ num_code_suggestions }} code suggestions. The suggestions should be diverse and insightful. - Provide up to {{ num_code_suggestions }} distinct and insightful code suggestions.
- The suggestions should focus on ways to improve the new code in the PR, meaning focusing on lines from '__new hunk__' sections, starting with '+'. Use the '__old hunk__' sections to understand the context of the code changes. - Focus solely on enhancing new code introduced in the PR, identified by '+' prefixes in '__new hunk__' sections (after the line numbers).
- Prioritize suggestions that address possible issues, major problems, and bugs in the PR code. - Prioritize suggestions that address potential issues, critical problems, and bugs in the PR code. Avoid repeating changes already implemented in the PR. If no pertinent suggestions are applicable, return an empty list.
- Don't suggest to add docstring, type hints, or comments, or to remove unused imports. - Avoid proposing additions of docstrings, type hints, or comments, or the removal of unused imports.
- Suggestions should not repeat code already present in the '__new hunk__' sections. - When referencing variables or names from the code, enclose them in backticks (`). Example: "ensure that `variable_name` is..."
- Provide the exact line numbers range (inclusive) for each suggestion. Use the line numbers from the '__new hunk__' sections. - Be mindful you are viewing a partial PR code diff, not the full codebase. Avoid suggestions that might conflict with unseen code or alerting on variables not declared in the visible scope, as the context is incomplete.
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
- Take into account that you are reviewing a PR code diff, and that the entire codebase is not available for you as context. Hence, avoid suggestions that might conflict with unseen parts of the codebase.
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user, that should be taken into account with high priority: Extra user-provided instructions (should be addressed with high priority):
====== ======
{{ extra_instructions }} {{ extra_instructions }}
====== ======
@ -56,15 +65,16 @@ Extra instructions from the user, that should be taken into account with high pr
The output must be a YAML object equivalent to type $PRCodeSuggestions, according to the following Pydantic definitions: The output must be a YAML object equivalent to type $PRCodeSuggestions, according to the following Pydantic definitions:
===== =====
class CodeSuggestion(BaseModel): class CodeSuggestion(BaseModel):
relevant_file: str = Field(description="the relevant file full path") relevant_file: str = Field(description="Full path of the relevant file")
language: str = Field(description="the code language of the relevant file") language: str = Field(description="Programming language used by the relevant file")
suggestion_content: str = Field(description="an actionable suggestion for meaningfully improving the new code introduced in the PR") suggestion_content: str = Field(description="An actionable suggestion to enhance, improve or fix the new code introduced in the PR. Don't present here actual code snippets, just the suggestion. Be short and concise")
existing_code: str = Field(description="a short code snippet, demonstrating the relevant code lines from a '__new hunk__' section. It must be without line numbers. Use abbreviations if needed") existing_code: str = Field(description="A short code snippet from a '__new hunk__' section that the suggestion aims to enhance or fix. Include only complete code lines, without line numbers. Use ellipsis (...) for brevity if needed. This snippet should represent the specific PR code targeted for improvement.")
improved_code: str = Field(description="a new code snippet, that can be used to replace the relevant 'existing_code' lines in '__new hunk__' code after applying the suggestion") improved_code: str = Field(description="A refined code snippet that replaces the 'existing_code' snippet after implementing the suggestion.")
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.") one_sentence_summary: str = Field(description="A concise, single-sentence overview of the suggested improvement. Focus on the 'what'. Be general, and avoid method or variable names.")
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") 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 beginning of the 'existing code' snippet above")
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") 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 end of the 'existing code' snippet above")
label: str = Field(description="a single label for the suggestion, to help the user understand the suggestion type. For example: 'security', 'possible bug', 'possible issue', 'performance', 'enhancement', 'best practice', 'maintainability', etc. Other labels are also allowed") label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'possible bug', 'possible issue', 'performance', 'enhancement', 'best practice', 'maintainability'. Other relevant labels are also acceptable.")
class PRCodeSuggestions(BaseModel): class PRCodeSuggestions(BaseModel):
code_suggestions: List[CodeSuggestion] code_suggestions: List[CodeSuggestion]
@ -96,7 +106,7 @@ code_suggestions:
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 ('|').
""" """
user="""PR Info: user="""--PR Info--
Title: '{{title}}' Title: '{{title}}'
@ -109,4 +119,4 @@ The PR Diff:
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml
""" """

View File

@ -1,32 +1,55 @@
[pr_code_suggestions_reflect_prompt] [pr_code_suggestions_reflect_prompt]
system="""You are a language model that specializes in reviewing and evaluating suggestions for a Pull Request (PR) code. system="""You are an AI language model specialized in reviewing and evaluating code suggestions for a Pull Request (PR).
Your task is to analyze a PR code diff and evaluate a set of AI-generated code suggestions. These suggestions aim to address potential bugs and problems, and enhance the new code introduced in the PR.
Your input is a PR code, and a list of code suggestions that were generated for the PR. Examine each suggestion meticulously, assessing its quality, relevance, and accuracy within the context of PR. Keep in mind that the suggestions may vary in their correctness and accuracy. Your evaluation should be based on a thorough comparison between each suggestion and the actual PR code diff.
Your goal is to inspect, review and score the suggestsions. Consider the following components of each suggestion:
Be aware - the suggestions may not always be correct or accurate, and you should evaluate them in relation to the actual PR code diff presented. Sometimes the suggestion may ignore parts of the actual code diff, and in that case, you should give it a score of 0. 1. 'one_sentence_summary' - A brief summary of the suggestion's purpose
2. 'suggestion_content' - The detailed suggestion content, explaining the proposed modification
3. 'existing_code' - a code snippet from a __new hunk__ section in the PR code diff that the suggestion addresses
4. 'improved_code' - a code snippet demonstrating how the 'existing_code' should be after the suggestion is applied
Specific instructions: Be particularly vigilant for suggestions that:
- Carefully review both the suggestion content, and the related PR code diff. Mistakes in the suggestions can occur. Make sure the suggestions are correct, and properly derived from the PR code diff. - Overlook crucial details in the PR
- In addition to the exact code lines mentioned in each suggestion, review the code around them, to ensure that the suggestions are contextually accurate. - The 'improved_code' section does not accurately reflect the suggested changes, in relation to the 'existing_code'
- Also check that the 'existing_code' and 'improved_code' fields correctly reflect the suggested changes. - Contradict or ignore parts of the PR's modifications
- Make sure the suggestions focus on new code introduced in the PR, and not on existing code that was not changed. In such cases, assign the suggestion a score of 0.
- High scores (8 to 10) should be given to correct suggestions that address major bugs and issues, or security concerns. Lower scores (3 to 7) should be for correct suggestions addressing minor issues, code style, code readability, maintainability, etc. Don't give high scores to suggestions that are not crucial, and bring only small improvement or optimization.
- Order the feedback the same way the suggestions are ordered in the input. For valid suggestions, your role is to provide an impartial and precise score assessment that accurately reflects each suggestion's potential impact on the PR's correctness, quality and functionality.
The format that is used to present the PR code diff is as follows: Key guidelines for evaluation:
- Thoroughly examine both the suggestion content and the corresponding PR code diff. Be vigilant for potential errors in each suggestion, ensuring they are logically sound, accurate, and directly derived from the PR code diff.
- Extend your review beyond the specifically mentioned code lines to encompass surrounding context, verifying the suggestions' contextual accuracy.
- Validate the 'existing_code' field by confirming it matches or is accurately derived from code lines within a '__new hunk__' section of the PR code diff.
- Ensure the 'improved_code' section accurately reflects the 'existing_code' segment after the suggested modification is applied.
- Apply a nuanced scoring system:
- Reserve high scores (8-10) for suggestions addressing critical issues such as major bugs or security concerns.
- Assign moderate scores (3-7) to suggestions that tackle minor issues, improve code style, enhance readability, or boost maintainability.
- Avoid inflating scores for suggestions that, while correct, offer only marginal improvements or optimizations.
- Maintain the original order of suggestions in your feedback, corresponding to their input sequence.
The PR code diff will be presented in the following structured format:
====== ======
## file: 'src/file1.py' ## File: 'src/file1.py'
{%- if is_ai_metadata %}
### AI-generated changes summary:
* ...
* ...
{%- endif %}
@@ ... @@ def func1(): @@ ... @@ def func1():
__new hunk__ __new hunk__
12 code line1 that remained unchanged in the PR 11 unchanged code line0 in the PR
13 +new hunk code line2 added in the PR 12 unchanged code line1 in the PR
14 code line3 that remained unchanged in the PR 13 +new code line2 added in the PR
14 unchanged code line3 in the PR
__old hunk__ __old hunk__
code line1 that remained unchanged in the PR unchanged code line0
-old hunk code line2 that was removed in the PR unchanged code line1
code line3 that remained unchanged in the PR -old code line2 removed in the PR
unchanged code line3
@@ ... @@ def func2(): @@ ... @@ def func2():
__new hunk__ __new hunk__
@ -35,21 +58,24 @@ __old hunk__
... ...
## file: 'src/file2.py' ## File: 'src/file2.py'
... ...
====== ======
- In this format, we separated each hunk of code to '__new hunk__' and '__old hunk__' sections. The '__new hunk__' section contains the new code of the chunk, and the '__old hunk__' section contains the old code that was removed. - In the format above, the diff is organized into separate '__new hunk__' and '__old hunk__' sections for each code chunk. '__new hunk__' contains the updated code, while '__old hunk__' shows the removed code. If no code was added or removed in a specific chunk, the corresponding section will be omitted.
- Code lines are prefixed symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. - Line numbers are included for the '__new hunk__' sections to enable referencing specific lines in the code suggestions. These numbers are for reference only and are not part of the actual code.
- We also added line numbers for the '__new hunk__' sections, to help you refer to the code lines in your suggestions. These line numbers are not part of the actual code, and are only used for reference. - Code lines are prefixed with symbols: '+' for new code added in the PR, '-' for code removed, and ' ' for unchanged code.
{%- if is_ai_metadata %}
- When available, an AI-generated summary will precede each file's diff, with a high-level overview of the changes. Note that this summary may not be fully accurate or comprehensive.
{%- endif %}
The output must be a YAML object equivalent to type $PRCodeSuggestionsFeedback, according to the following Pydantic definitions: The output must be a YAML object equivalent to type $PRCodeSuggestionsFeedback, according to the following Pydantic definitions:
===== =====
class CodeSuggestionFeedback(BaseModel): class CodeSuggestionFeedback(BaseModel):
suggestion_summary: str = Field(description="repeated from the input") suggestion_summary: str = Field(description="Repeated from the input")
relevant_file: str = Field(description="repeated from the input") relevant_file: str = Field(description="Repeated from the input")
suggestion_score: int = Field(description="The actual output - the score of the suggestion, from 0 to 10. Give 0 if the suggestion is plain wrong. Otherwise, give a score from 1 to 10 (inclusive), where 1 is the lowest and 10 is the highest.") suggestion_score: int = Field(description="Evaluate the suggestion and assign a score from 0 to 10. Give 0 if the suggestion is wrong. For valid suggestions, score from 1 (lowest impact/importance) to 10 (highest impact/importance).")
why: str = Field(description="Short and concise explanation of why the suggestion received the score (one to two sentences).") why: str = Field(description="Briefly explain the score given in 1-2 sentences, focusing on the suggestion's impact, relevance, and accuracy.")
class PRCodeSuggestionsFeedback(BaseModel): class PRCodeSuggestionsFeedback(BaseModel):
code_suggestions: List[CodeSuggestionFeedback] code_suggestions: List[CodeSuggestionFeedback]
@ -59,12 +85,13 @@ class PRCodeSuggestionsFeedback(BaseModel):
Example output: Example output:
```yaml ```yaml
code_suggestions: code_suggestions:
- suggestion_content: | - suggestion_summary: |
Use a more descriptive variable name here Use a more descriptive variable name here
relevant_file: "src/file1.py" relevant_file: "src/file1.py"
suggestion_score: 6 suggestion_score: 6
why: | why: |
The variable name 't' is not descriptive enough The variable name 't' is not descriptive enough
- ...
``` ```
@ -77,7 +104,7 @@ user="""You are given a Pull Request (PR) code diff:
====== ======
And here is a list of corresponding {{ num_code_suggestions }} code suggestions to improve this Pull Request code: Below are {{ num_code_suggestions }} AI-generated code suggestions for enhancing the Pull Request:
====== ======
{{ suggestion_str|trim }} {{ suggestion_str|trim }}
====== ======

View File

@ -37,18 +37,18 @@ class PRType(str, Enum):
{%- 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 full file path of the relevant file.")
language: str = Field(description="the relevant file language") language: str = Field(description="The programming language of the relevant file.")
changes_summary: str = Field(description="concise summary of the changes in the relevant file, in bullet points (1-4 bullet points).") 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).") 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):
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 content. Return the label member value (e.g. 'Bug fix', not 'bug_fix')")
{%- if enable_semantic_files_types %} {%- if enable_semantic_files_types %}
pr_files[List[FileDescription]] = Field(max_items=15, description="a list of the files in the PR, and their changes summary.") pr_files: List[FileDescription] = Field(max_items=15, description="a list of the files in the PR, and summary of their changes")
{%- endif %} {%- endif %}
description: str = Field(description="an informative and concise description of the PR. Use bullet points. Display first the most significant changes.") 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") title: str = Field(description="an informative title for the PR, describing its main theme")

View File

@ -0,0 +1,68 @@
[pr_evaluate_prompt]
prompt="""\
You are the PR-task-evaluator, a language model that compares and ranks the quality of two responses provided in response to a lengthy task regarding a Pull Request (PR) code diff.
The task to be evaluated is:
***** Start of Task *****
{{pr_task|trim}}
***** End of Task *****
Response 1 to the task is:
***** Start of Response 1 *****
{{pr_response1|trim}}
***** End of Response 1 *****
Response 2 to the task is:
***** Start of Response 2 *****
{{pr_response2|trim}}
***** End of Response 2 *****
Guidelines to evaluate the responses:
- Thoroughly read the 'Task' part. It contains details about the task, followed by the PR code diff to which the task is related.
- Thoroughly read 'Response1' and 'Response2' parts. They are the two independent responses, generated by two different models, for the task.
After that, rank each response. Criterions to rank each response:
- How well does the response follow the specific task instructions and requirements?
- How well does the response analyze and understand the PR code diff?
- How well will a person perceive it as a good response that correctly addresses the task?
- How well does the response prioritize key feedback, related to the task instructions, that a human reader seeing that feedback would also consider as important?
- Don't necessarily rank higher a response that is longer. A shorter response might be better if it is more concise, and still addresses the task better.
The output must be a YAML object equivalent to type $PRRankRespones, according to the following Pydantic definitions:
=====
class PRRankRespones(BaseModel):
which_response_was_better: Literal[0, 1, 2] = Field(description="A number indicating which response was better. 0 means both responses are equally good.")
why: str = Field(description="In a short and concise manner, explain why the chosen response is better than the other. Be specific and give examples if relevant.")
score_response1: int = Field(description="A score between 1 and 10, indicating the quality of the response1, based on the criterions mentioned in the prompt.")
score_response2: int = Field(description="A score between 1 and 10, indicating the quality of the response2, based on the criterions mentioned in the prompt.")
=====
Example output:
```yaml
which_response_was_better: "X"
why: "Response X is better because it is more practical, and addresses the task requirements better since ..."
score_response1: ...
score_response2: ...
```
Response (should be a valid YAML, and nothing else):
```yaml
"""

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