mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-11 08:10:38 +08:00
Compare commits
442 Commits
v0.23
...
mrT23-patc
Author | SHA1 | Date | |
---|---|---|---|
621cfe5595 | |||
b447080777 | |||
da398ce56f | |||
16763d81b4 | |||
80fe297bc9 | |||
5d68b0c492 | |||
8d5f015e5c | |||
be03f83318 | |||
cbfd250c0c | |||
7ce46e65a1 | |||
600f230ba7 | |||
4f4f13b8b2 | |||
146b8823a9 | |||
fdb1ff8057 | |||
ce8e637800 | |||
306af02d22 | |||
a23541912b | |||
0851767774 | |||
585a7f1c69 | |||
8d82cb2e04 | |||
7586514abf | |||
480a025877 | |||
8f943a0d44 | |||
2102c51422 | |||
29028d43cf | |||
95d1b0d0c5 | |||
cc0e432247 | |||
0fb158fd47 | |||
867a430a38 | |||
a94496285f | |||
567c144176 | |||
c08b59a74d | |||
0ba81e1ac7 | |||
2cb0dd2496 | |||
a8367d1a22 | |||
1a3345c6e6 | |||
564845adff | |||
3ea691e70a | |||
5047d076f8 | |||
7de6bb0150 | |||
a1582b5338 | |||
dd8d78e7d8 | |||
5af6cc7538 | |||
6cc562d6a2 | |||
19b051b992 | |||
be68ee89f3 | |||
db6c75a130 | |||
74688846e0 | |||
09b0a04a47 | |||
cc1b65f886 | |||
1451d82d6b | |||
01ba6fe63d | |||
74f9da1135 | |||
f80c2ae2c8 | |||
e444da8378 | |||
25ad8a09ce | |||
897e791b1a | |||
7f94dda54e | |||
538a592882 | |||
a3cb7277a7 | |||
b5cd560402 | |||
fd38c33fcb | |||
f767a3dfde | |||
9f8b619858 | |||
8de16939ba | |||
6ed5537065 | |||
1a9638cf87 | |||
49521aafff | |||
c8e8ed89d2 | |||
ebc5cafb2b | |||
52e8d7bc6a | |||
f7344fd787 | |||
86103c65e8 | |||
a4658b9960 | |||
5fd831c448 | |||
332d3a0c5e | |||
edef712b6a | |||
1831f2cec4 | |||
8706f643ef | |||
35a75095ea | |||
0aa296d03e | |||
24f7e8622f | |||
d01cfe443c | |||
6150256040 | |||
147a8e0ef3 | |||
9199d84796 | |||
39913ef12a | |||
d2a744e70c | |||
be93c52380 | |||
7ccefca35e | |||
14b4723734 | |||
c8f1c03061 | |||
b02fa22948 | |||
85754d2d79 | |||
f0d780c7ec | |||
19048ee705 | |||
b8d2b263b9 | |||
6f17c08f72 | |||
65c0bc414f | |||
015719134f | |||
1ed6b7a54a | |||
14067a02db | |||
be75bb6a16 | |||
883d945687 | |||
8090115f30 | |||
6fa226dee7 | |||
13c1cdbf90 | |||
d4d9a7f8b4 | |||
c14c49727f | |||
292a5015d6 | |||
2f7f60a469 | |||
adce35765b | |||
6776f7c296 | |||
7287a94e88 | |||
e2cf1d0068 | |||
8ada3111ec | |||
9c9611e81a | |||
4fb93e3b62 | |||
5a27e1dd7e | |||
6e6151d201 | |||
e468efb53e | |||
95e1ebada1 | |||
d74c867eca | |||
2448281a45 | |||
9e063bf48a | |||
5432469ef6 | |||
2c496b9d4e | |||
5ac41dddd6 | |||
9df554ed1c | |||
23af1afa03 | |||
fdcbdfce98 | |||
cf14e45674 | |||
1c51b5b762 | |||
e5715e12cb | |||
578d7c69f8 | |||
29c50758bc | |||
97b48da03b | |||
4203ee4ca8 | |||
84dc976ebb | |||
d9571ee7cb | |||
7373ed36e6 | |||
cdf13925b0 | |||
c2f52539aa | |||
0442cdcd3d | |||
93773f3c08 | |||
53a974c282 | |||
c9ed271eaf | |||
6a5ff2fa3b | |||
25d661c152 | |||
d20c9c6c94 | |||
d1d861e163 | |||
033db1015e | |||
abf2f68c61 | |||
441e098e2a | |||
2bbf4b366e | |||
b9d096187a | |||
ce156751e8 | |||
dae87d7da8 | |||
a99ebf8953 | |||
2a9e3ee1ef | |||
2beefab89a | |||
415f44d763 | |||
8fb9b8ed3e | |||
4f1dccf67b | |||
3778cc2745 | |||
8793f8d9b0 | |||
61837c69a3 | |||
ffaf5d5271 | |||
cd526a233c | |||
745e955d1f | |||
771d0b8c60 | |||
91a7c08546 | |||
4d9d6f7477 | |||
2591a5d6c1 | |||
772499fce1 | |||
d467f5a7fd | |||
2d5b060168 | |||
b7eb6be5a0 | |||
df57367426 | |||
660a60924e | |||
8aa76a0ac5 | |||
b034d16c23 | |||
9bec97c66c | |||
8fd8d298e7 | |||
2e186ebae8 | |||
fc40ca9196 | |||
91a8938a37 | |||
d97e1862da | |||
f042c061de | |||
c47afd9c0d | |||
c6d16ced07 | |||
e9535ea164 | |||
dc8a4be2d4 | |||
f9de8f283b | |||
bd5c19ee05 | |||
7cbe797108 | |||
435d9d41c8 | |||
a510d93e6e | |||
48cc2f6833 | |||
229d7b34c7 | |||
03b194c337 | |||
a6f772c6d5 | |||
ba1ba98dec | |||
5954c7cec2 | |||
dc1a8e8314 | |||
aa87bc60f6 | |||
c76aabc71e | |||
81081186d9 | |||
4a71ec90c6 | |||
3456c8e039 | |||
402a388be0 | |||
4e26c02b01 | |||
ea4f88edd3 | |||
217f615dfb | |||
a6fb351789 | |||
bfab660414 | |||
2e63653bb0 | |||
b9df034c97 | |||
bae8d36698 | |||
67a04e1cb2 | |||
4fea780b9b | |||
01c18d7d98 | |||
f4b06640d2 | |||
f1981092d3 | |||
8414e109c5 | |||
8adfca5b3c | |||
672cdc03ab | |||
86a9cfedc8 | |||
7ac9f27b70 | |||
c97c39d57d | |||
a3b3d6c77a | |||
2e41701d07 | |||
578f56148a | |||
b3da84b4aa | |||
f89bdcf3c3 | |||
e7e3970874 | |||
1f7a8eada0 | |||
38638bd1c4 | |||
26f3bd8900 | |||
a2fb415c53 | |||
8038eaf876 | |||
d8572f8d13 | |||
78b11c80c7 | |||
cb65b05e85 | |||
1aa6dd9b5d | |||
11d69e05aa | |||
0722af4702 | |||
99e99345b2 | |||
5252e1826d | |||
a18a0bf2e3 | |||
396d11aa45 | |||
4a38861d06 | |||
5feb66597e | |||
8589941ffe | |||
7f0e6aeb37 | |||
8a768aa7fd | |||
f399f9ebe4 | |||
cc73d4599b | |||
4228f92e7e | |||
1f4ab43fa6 | |||
b59111e4a6 | |||
70da871876 | |||
9c1ab06491 | |||
5c4bc0a008 | |||
ef37271ce9 | |||
8dd4c15d4b | |||
f9afada1ed | |||
4c1c313031 | |||
1f126069b1 | |||
12742ef499 | |||
63e921a2c5 | |||
8f04387331 | |||
a06670bc27 | |||
2525392814 | |||
23aa2a9388 | |||
e85b75fe64 | |||
df04a7e046 | |||
9c3f080112 | |||
ed65493718 | |||
983233c193 | |||
7438190ed1 | |||
2b2b851cb9 | |||
5701816b2e | |||
40a25a1082 | |||
e238a88824 | |||
61bdfd3b99 | |||
c00d1e9858 | |||
1a8b143f58 | |||
dfbe7432b8 | |||
ab69f1769b | |||
089210d9fa | |||
0f9d89c67a | |||
84b80f792d | |||
219d962cbe | |||
e531245f4a | |||
89e9413d75 | |||
b370cb6ae7 | |||
4201779ce2 | |||
71f7c09ed7 | |||
edad244a86 | |||
9752987966 | |||
200da44e5a | |||
4c0fd37ac2 | |||
c996c7117f | |||
943ba95924 | |||
8a75d3101d | |||
944f54b431 | |||
9be5cc6dec | |||
884286ebf1 | |||
620dbbeb1a | |||
c07059e139 | |||
3b88d6afdb | |||
e717e8ae81 | |||
8ec1fb5937 | |||
cb10ceadd7 | |||
96d3f3cc0b | |||
a98d972041 | |||
09a1d74a00 | |||
31f6f8f8ea | |||
e7c99f0e6f | |||
ac53e6728d | |||
b100e7098a | |||
2b77d07725 | |||
ee1676cf7e | |||
3420e6f30d | |||
85cc0ad08c | |||
3756b547da | |||
e34bcace29 | |||
2a675b80ca | |||
1cefd23739 | |||
aef9a04b32 | |||
fe4e642a47 | |||
039d85b836 | |||
0fa342ddd2 | |||
c95a8cde72 | |||
23ec25c949 | |||
9560bc1b44 | |||
346ea8fbae | |||
d671c78233 | |||
240e0374e7 | |||
288e9bb8ca | |||
d8545a2b28 | |||
95f23de7ec | |||
0390a85f5a | |||
172d0c0358 | |||
41588efe9a | |||
f50832e19b | |||
927f124dca | |||
232b540f60 | |||
452eda25cd | |||
110e593d03 | |||
af84409c1d | |||
c2c69f2950 | |||
e946a0ea9f | |||
866476080c | |||
27d6560de8 | |||
6ba7b3eea2 | |||
86d9612882 | |||
49f608c968 | |||
11f85cad62 | |||
5f5257d195 | |||
495e2ccb7d | |||
a176adb23e | |||
68ef11a2fc | |||
38c38ec280 | |||
3904eebf85 | |||
778d7ce1ed | |||
3067afbcb3 | |||
70f7a90226 | |||
7eadb45c09 | |||
ac247dbc2c | |||
3a77652660 | |||
0bd4c9b78a | |||
81d07a55d7 | |||
652ced5406 | |||
aaf037570b | |||
cfa565b5d7 | |||
c8819472cf | |||
53744af32f | |||
41c6502190 | |||
32604d8103 | |||
581c95c4ab | |||
789c48a216 | |||
6b9de6b253 | |||
003846a90d | |||
d088f9c19a | |||
a272c761a9 | |||
9449f2aebe | |||
28ea4a685a | |||
b798291bc8 | |||
62df50cf86 | |||
917e1607de | |||
8f11a19c32 | |||
0f5cccd18f | |||
2be459e576 | |||
cbdb451c95 | |||
6871193381 | |||
8a7f3501ea | |||
80bbe23ad5 | |||
05f3fa5ebc | |||
1b2a2075ae | |||
3d3b49e3ee | |||
174b4b76eb | |||
2b28153749 | |||
6151bfac25 | |||
5d6e1de157 | |||
ce35d2c313 | |||
b51abe9af7 | |||
20206af1bf | |||
34ae1f1ab6 | |||
887283632b | |||
7f84b5738e | |||
dc917587ef | |||
b2710ec029 | |||
41c48ca5b5 | |||
e0012702c6 | |||
dfb339ab44 | |||
54947573bf | |||
228ceff3a6 | |||
8766140554 | |||
034ec8f53a | |||
eccd00b86f | |||
4b351cfe38 | |||
734a027702 | |||
d0948329d3 | |||
6135bf1f53 | |||
ea9deccb91 | |||
daa68f3b2f | |||
e82430891c | |||
19ca7f887a | |||
888306c160 | |||
3ef4daafd5 | |||
f76f750757 | |||
055bc4ceec | |||
487efa4bf4 | |||
050ffcdd06 | |||
8f9879cf01 | |||
c3fac86067 | |||
9a57d00951 | |||
745d0c537c | |||
5b594dadee | |||
4246792261 |
2
.github/workflows/build-and-test.yaml
vendored
2
.github/workflows/build-and-test.yaml
vendored
@ -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
54
.github/workflows/code_coverage.yaml
vendored
Normal 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
46
.github/workflows/e2e_tests.yaml
vendored
Normal 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
|
@ -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"
|
||||||
|
103
README.md
103
README.md
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
</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>
|
||||||
|
|
||||||
[](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
|
[](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
|
||||||
@ -18,6 +18,7 @@ CodiumAI PR-Agent aims to help efficiently review and handle pull requests, by p
|
|||||||
[](https://pr-agent-docs.codium.ai/finetuning_benchmark/)
|
[](https://pr-agent-docs.codium.ai/finetuning_benchmark/)
|
||||||
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
||||||
[](https://twitter.com/codiumai)
|
[](https://twitter.com/codiumai)
|
||||||
|
[](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>
|
||||||
@ -36,31 +37,44 @@ 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
|
||||||
|
|
||||||
### July 4, 2024
|
### September 12, 2024
|
||||||
|
[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.
|
||||||
|
|
||||||
Added improved support for claude-sonnet-3.5 model (anthropic, vertex, bedrock), including dedicated prompts.
|
### September 3, 2024
|
||||||
|
|
||||||
### June 17, 2024
|
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.
|
||||||
|
|
||||||
New option for a self-review checkbox is now available for the `/improve` tool, along with the ability(💎) to enable auto-approve, or demand self-review in addition to human reviewer. See more [here](https://pr-agent-docs.codium.ai/tools/improve/#self-review).
|
### August 26, 2024
|
||||||
|
|
||||||
<kbd><img src="https://www.codium.ai/images/pr_agent/self_review_1.png" width="512"></kbd>
|
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).
|
||||||
|
|
||||||
### June 6, 2024
|
<kbd><img src="https://www.codium.ai/images/pr_agent/pr_chat_1.png" width="768"></kbd>
|
||||||
|
|
||||||
New option now available (💎) - **apply suggestions**:
|
<kbd><img src="https://www.codium.ai/images/pr_agent/pr_chat_2.png" width="768"></kbd>
|
||||||
|
|
||||||
<kbd><img src="https://www.codium.ai/images/pr_agent/apply_suggestion_1.png" width="512"></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)
|
||||||
|
|
||||||
<kbd><img src="https://www.codium.ai/images/pr_agent/apply_suggestion_2.png" width="512"></kbd>
|
### 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -70,39 +84,39 @@ New option now available (💎) - **apply suggestions**:
|
|||||||
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](./docs/Full_environments.md))
|
[//]: # (- Support for additional git providers is described in [here](./docs/Full_environments.md))
|
||||||
@ -221,7 +235,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.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -231,43 +249,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](https://platform.openai.com/), with access to GPT-4.)
|
|
||||||
|
|
||||||
[//]: # (2. A GitHub personal access token (classic) with the repo scope.)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (There are several ways to use PR-Agent:)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (**Locally**)
|
|
||||||
|
|
||||||
[//]: # (- [Using pip package](https://pr-agent-docs.codium.ai/installation/locally/#using-pip-package))
|
|
||||||
|
|
||||||
[//]: # (- [Using Docker image](https://pr-agent-docs.codium.ai/installation/locally/#using-docker-image))
|
|
||||||
|
|
||||||
[//]: # (- [Run from source](https://pr-agent-docs.codium.ai/installation/locally/#run-from-source))
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (**GitHub specific methods**)
|
|
||||||
|
|
||||||
[//]: # (- [Run as a GitHub Action](https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-action))
|
|
||||||
|
|
||||||
[//]: # (- [Run as a GitHub App](https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-app))
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (**GitLab specific methods**)
|
|
||||||
|
|
||||||
[//]: # (- [Run a GitLab webhook server](https://pr-agent-docs.codium.ai/installation/gitlab/))
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (**BitBucket specific methods**)
|
|
||||||
|
|
||||||
[//]: # (- [Run as a Bitbucket Pipeline](https://pr-agent-docs.codium.ai/installation/bitbucket/))
|
|
||||||
|
|
||||||
## 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:
|
||||||
|
5
codecov.yml
Normal file
5
codecov.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
comment: false
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
patch: false
|
||||||
|
project: false
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10 as base
|
FROM python:3.12.3 AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD pyproject.toml .
|
ADD pyproject.toml .
|
||||||
@ -6,36 +6,36 @@ 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", "-m", "gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "pr_agent/servers/gunicorn_config.py", "--forwarded-allow-ips", "*", "pr_agent.servers.github_app:app"]
|
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"]
|
||||||
|
5
docs/docs/chrome-extension/data_privacy.md
Normal file
5
docs/docs/chrome-extension/data_privacy.md
Normal 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 PR-Agent Pro integration.
|
||||||
|
|
51
docs/docs/chrome-extension/features.md
Normal file
51
docs/docs/chrome-extension/features.md
Normal 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 PR-Agent 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 PR-Agent 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
|
||||||
|
|
||||||
|
PR-Agent 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 PR-Agent 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">
|
||||||
|
|
||||||
|
### PR-Agent filters
|
||||||
|
|
||||||
|
PR-Agent 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 PR-Agent, 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
|
||||||
|
|
||||||
|
PR-Agent 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">
|
@ -1,49 +1,14 @@
|
|||||||
## PR-Agent chrome extension
|
[PR-Agent 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 collection of tools that integrates seamlessly with your GitHub environment, aiming to enhance your PR-Agent usage experience, and providing additional features.
|
|
||||||
|
|
||||||
## Features
|
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, PR-Agent filters, and additional abilities.
|
||||||
|
|
||||||
### Toolbar extension
|
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.
|
||||||
With PR-Agent 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.
|
|
||||||
|
|
||||||
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.
|
For private repositories, you will need to install [PR-Agent 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 PR-Agent 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).
|
||||||
|
|
||||||
<img src="https://codium.ai/images/pr_agent/toolbar1.png" width="512">
|
<img src="https://codium.ai/images/pr_agent/PR-AgentChat.gif" width="768">
|
||||||
|
|
||||||
<img src="https://codium.ai/images/pr_agent/toolbar2.png" width="512">
|
### Supported browsers
|
||||||
|
|
||||||
### PR-Agent filters
|
The extension is supported on all Chromium-based browsers, including Google Chrome, Arc, Opera, Brave, and Microsoft Edge.
|
||||||
|
|
||||||
PR-Agent 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 PR-Agent, 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
|
|
||||||
|
|
||||||
PR-Agent 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">
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Go to the marketplace and install the extension:
|
|
||||||
[PR-Agent Chrome Extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl)
|
|
||||||
|
|
||||||
## Pre-requisites
|
|
||||||
|
|
||||||
The PR-Agent Chrome extension will work on any repo where you have previously [installed PR-Agent](https://pr-agent-docs.codium.ai/installation/).
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
2
docs/docs/core-abilities/code_oriented_yaml.md
Normal file
2
docs/docs/core-abilities/code_oriented_yaml.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
## Overview
|
||||||
|
TBD
|
47
docs/docs/core-abilities/compression_strategy.md
Normal file
47
docs/docs/core-abilities/compression_strategy.md
Normal 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
|
||||||
|
|
||||||
|
{width=768}
|
72
docs/docs/core-abilities/dynamic_context.md
Normal file
72
docs/docs/core-abilities/dynamic_context.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
## TL;DR
|
||||||
|
|
||||||
|
PR-Agent 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, PR-Agent 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, PR-Agent 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, PR-Agent 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
|
||||||
|
```
|
44
docs/docs/core-abilities/impact_evaluation.md
Normal file
44
docs/docs/core-abilities/impact_evaluation.md
Normal 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, PR-Agent 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 PR-Agent Suggestions
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
When a user pushes a new commit to the pull request, PR-Agent 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:** PR-Agent 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, PR-Agent automatically adds a ✅ (check mark) to the relevant suggestion, enabling transparent tracking of PR-Agent's impact analysis.
|
||||||
|
PR-Agent will also add, inside the relevant suggestions, an explanation of how the new code was impacted by each suggestion.
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
### Dashboard Metrics
|
||||||
|
The dashboard provides macro-level insights into the overall impact of PR-Agent on the pull-request process with key productivity metrics.
|
||||||
|
|
||||||
|
By offering clear, data-driven evidence of PR-Agent'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:
|
||||||
|
|
||||||
|
#### PR-Agent Impacts per 1K Lines
|
||||||
|
{width=512}
|
||||||
|
> Explanation: for every 1K lines of code (additions/edits), PR-Agent 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 PR-Agent 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 PR-Agent 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 PR-Agent's return on investment to stakeholders.
|
||||||
|
|
||||||
|
#### Suggestion Effectiveness Across Categories
|
||||||
|
{width=512}
|
||||||
|
> Explanation: This chart illustrates the distribution of implemented suggestions across different categories, enabling teams to better understand PR-Agent's impact on various aspects of code quality and development practices.
|
||||||
|
|
||||||
|
#### Suggestion Score Distribution
|
||||||
|
{width=512}
|
||||||
|
> Explanation: The distribution of the suggestion score for the implemented suggestions, ensuring that higher-scored suggestions truly represent more significant improvements.
|
@ -1,52 +1,12 @@
|
|||||||
## PR Compression Strategy
|
# Core Abilities
|
||||||
There are two scenarios:
|
PR-Agent 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://pr-agent-docs.codium.ai/core-abilities/metadata/)
|
||||||
2. The PR is too large to fit in a single prompt (including system and user prompt)
|
- [Dynamic context](https://pr-agent-docs.codium.ai/core-abilities/dynamic_context/)
|
||||||
|
- [Self-reflection](https://pr-agent-docs.codium.ai/core-abilities/self_reflection/)
|
||||||
For both scenarios, we first use the following strategy
|
- [Impact evaluation](https://pr-agent-docs.codium.ai/core-abilities/impact_evaluation/)
|
||||||
|
- [Interactivity](https://pr-agent-docs.codium.ai/core-abilities/interactivity/)
|
||||||
#### Repo language prioritization strategy
|
- [Compression strategy](https://pr-agent-docs.codium.ai/core-abilities/compression_strategy/)
|
||||||
We prioritize the languages of the repo based on the following criteria:
|
- [Code-oriented YAML](https://pr-agent-docs.codium.ai/core-abilities/code_oriented_yaml/)
|
||||||
|
- [Static code analysis](https://pr-agent-docs.codium.ai/core-abilities/static_code_analysis/)
|
||||||
1. Exclude binary files and non code files (e.g. images, pdfs, etc)
|
- [Code fine-tuning benchmark](https://pr-agent-docs.codium.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
|
|
||||||
|
|
||||||
{width=768}
|
|
||||||
|
|
||||||
## YAML Prompting
|
|
||||||
TBD
|
|
||||||
|
|
||||||
## Static Code Analysis 💎
|
|
||||||
TBD
|
|
2
docs/docs/core-abilities/interactivity.md
Normal file
2
docs/docs/core-abilities/interactivity.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
## Interactive invocation 💎
|
||||||
|
TBD
|
56
docs/docs/core-abilities/metadata.md
Normal file
56
docs/docs/core-abilities/metadata.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
## Local and global metadata injection with multi-stage analysis
|
||||||
|
(1)
|
||||||
|
PR-Agent 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, PR-Agent can incorporate supplementary preferences provided by the user, like [`extra_instructions` and `organization best practices`](https://pr-agent-docs.codium.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 PR-Agent executes is [`describe`](https://pr-agent-docs.codium.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, PR-Agent 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://pr-agent-docs.codium.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 PR-Agent AI models to generate more precise and contextually relevant suggestions and feedback.
|
51
docs/docs/core-abilities/self_reflection.md
Normal file
51
docs/docs/core-abilities/self_reflection.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
## TL;DR
|
||||||
|
|
||||||
|
PR-Agent 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, PR-Agent 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 PR-Agent 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
|
||||||
|
|
||||||
|
{width=768}
|
||||||
|
{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)
|
||||||
|
```
|
70
docs/docs/core-abilities/static_code_analysis.md
Normal file
70
docs/docs/core-abilities/static_code_analysis.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
## Overview - Static Code Analysis 💎
|
||||||
|
|
||||||
|
By combining static code analysis with LLM capabilities, PR-Agent 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://pr-agent-docs.codium.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:
|
||||||
|
|
||||||
|
{width=768}
|
||||||
|
|
||||||
|
Clicking on each checkbox will trigger the relevant tool for the selected component.
|
||||||
|
|
||||||
|
### Generate Tests
|
||||||
|
|
||||||
|
The [`test`](https://pr-agent-docs.codium.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.
|
||||||
|
|
||||||
|
{width=768}
|
||||||
|
|
||||||
|
### Generate Docs for a Component
|
||||||
|
|
||||||
|
The [`add_docs`](https://pr-agent-docs.codium.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.
|
||||||
|
|
||||||
|
{width=768}
|
||||||
|
|
||||||
|
### Generate Code Suggestions for a Component
|
||||||
|
The [`improve_component`](https://pr-agent-docs.codium.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.
|
||||||
|
|
||||||
|
{width=768}
|
||||||
|
|
||||||
|
### Find Similar Code
|
||||||
|
|
||||||
|
The [`similar code`](https://pr-agent-docs.codium.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`:
|
||||||
|
|
||||||
|
{width=768}
|
@ -11,6 +11,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {*/
|
/*.md-nav__title, .md-nav__link {*/
|
||||||
/* font-size: 18px;*/
|
/* font-size: 18px;*/
|
||||||
/* margin-top: 14px; !* Adjust the space as needed *!*/
|
/* margin-top: 14px; !* Adjust the space as needed *!*/
|
||||||
|
67
docs/docs/faq/index.md
Normal file
67
docs/docs/faq/index.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# FAQ
|
||||||
|
|
||||||
|
??? note "Question: Can PR-Agent serve as a substitute for a human reviewer?"
|
||||||
|
#### Answer:<span style="display:none;">1</span>
|
||||||
|
|
||||||
|
PR-Agent 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.
|
||||||
|
PR-Agent aims to address these pain points, and to assist and empower both the PR author and reviewer.
|
||||||
|
|
||||||
|
However, PR-Agent 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://pr-agent-docs.codium.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://pr-agent-docs.codium.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://pr-agent-docs.codium.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. PR-Agent 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://pr-agent-docs.codium.ai/overview/data_privacy/)
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
??? note "Question: Can I use my own LLM keys with PR-Agent?"
|
||||||
|
#### Answer:<span style="display:none;">5</span>
|
||||||
|
|
||||||
|
When you self-host, you use your own keys.
|
||||||
|
|
||||||
|
PR-Agent Pro with SaaS deployment is a hosted version of PR-Agent, where Codium AI 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.
|
||||||
|
|
||||||
|
___
|
@ -23,6 +23,7 @@ Here are the results:
|
|||||||
| QWEN-1.5-32B | 32 | 29 |
|
| QWEN-1.5-32B | 32 | 29 |
|
||||||
| | | |
|
| | | |
|
||||||
| **CodeQwen1.5-7B** | **7** | **35.4** |
|
| **CodeQwen1.5-7B** | **7** | **35.4** |
|
||||||
|
| Llama-3.1-8B-Instruct | 8 | 35.2 |
|
||||||
| Granite-8b-code-instruct | 8 | 34.2 |
|
| Granite-8b-code-instruct | 8 | 34.2 |
|
||||||
| CodeLlama-7b-hf | 7 | 31.8 |
|
| CodeLlama-7b-hf | 7 | 31.8 |
|
||||||
| Gemma-7B | 7 | 27.2 |
|
| Gemma-7B | 7 | 27.2 |
|
||||||
|
@ -16,19 +16,19 @@ PR-Agent offers extensive pull request functionalities across various git provid
|
|||||||
|-------|-----------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|
|
|-------|-----------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|
|
||||||
| TOOLS | Review | ✅ | ✅ | ✅ | ✅ |
|
| TOOLS | Review | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | ⮑ Incremental | ✅ | | | |
|
| | ⮑ Incremental | ✅ | | | |
|
||||||
| | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.ai/tools/review/#soc2-ticket-compliance){:target="_blank"} 💎 | ✅ | ✅ | ✅ | ✅ |
|
| | ⮑ [SOC2 Compliance](https://pr-agent-docs.codium.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://pr-agent-docs.codium.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 | ✅ | ✅ | ✅ | ✅ |
|
||||||
@ -39,8 +39,8 @@ PR-Agent offers extensive pull request functionalities across various git provid
|
|||||||
| | 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 [PR-Agent Pro](https://www.codium.ai/pricing/){:target="_blank"}
|
||||||
|
|
||||||
@ -78,4 +78,4 @@ The following diagram illustrates PR-Agent tools and their flow:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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 PR-Agent.
|
||||||
|
@ -1,4 +1,62 @@
|
|||||||
## Azure DevOps provider
|
## Azure DevOps Pipeline
|
||||||
|
You can use a pre-built Action Docker image to run PR-Agent 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 PR Agent'
|
||||||
|
```
|
||||||
|
This script will run PR-Agent 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):
|
||||||
|
{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:
|
||||||
```
|
```
|
||||||
|
@ -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 [PR-Agent 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
|
||||||
|
@ -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`:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -1,3 +1,44 @@
|
|||||||
|
## Run as a GitLab Pipeline
|
||||||
|
You can use a pre-built Action Docker image to run PR-Agent as a GitLab pipeline. This is a simple way to get started with PR-Agent 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__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 PR-Agent on every new merge request. You can modify the `rules` section to run PR-Agent on different events.
|
||||||
|
You can also modify the `script` section to run different PR-Agent 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.
|
||||||
@ -14,7 +55,7 @@ WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
|
|||||||
- 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.
|
@ -3,7 +3,7 @@
|
|||||||
## Self-hosted PR-Agent
|
## Self-hosted PR-Agent
|
||||||
If you choose to host you own PR-Agent, you first need to acquire two tokens:
|
If you choose to host you own PR-Agent, you first need to acquire two tokens:
|
||||||
|
|
||||||
1. An OpenAI key from [here](https://platform.openai.com/api-keys), with access to GPT-4 (or a key for [other models](../usage-guide/additional_configurations.md/#changing-a-model), if you prefer).
|
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://pr-agent-docs.codium.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 PR-Agent:
|
||||||
@ -15,8 +15,7 @@ There are several ways to use self-hosted PR-Agent:
|
|||||||
- [Azure DevOps](./azure.md)
|
- [Azure DevOps](./azure.md)
|
||||||
|
|
||||||
## PR-Agent Pro 💎
|
## PR-Agent Pro 💎
|
||||||
PR-Agent Pro, an app for GitHub\GitLab\BitBucket hosted by CodiumAI, is also available.
|
PR-Agent 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 PR-Agent Pro, installation is as simple as signing up and adding the PR-Agent app to your relevant repo.
|
||||||
<br>
|
See [here](https://pr-agent-docs.codium.ai/installation/pr_agent_pro/) for more details.
|
||||||
See [here](./pr_agent_pro.md) for more details.
|
|
@ -16,6 +16,10 @@ Once a user acquires a seat, they gain the flexibility to use PR-Agent Pro acros
|
|||||||
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 PR-Agent 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, PR-Agent Pro will cease to respond to their inquiries unless a seat is purchased.
|
||||||
|
|
||||||
|
## Install PR-Agent Pro for GitHub Enterprise Server
|
||||||
|
You can install PR-Agent Pro application on your GitHub Enterprise Server, and enjoy two weeks of free trial.
|
||||||
|
After the trial period, to continue using PR-Agent 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 PR-Agent Pro for GitLab (Teams & Enterprise)
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
## Self-hosted PR-Agent
|
## 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:
|
- If you self-host PR-Agent with your OpenAI (or other LLM provider) API key, it is between you and the provider. We don't send your code data to PR-Agent servers.
|
||||||
https://openai.com/enterprise-privacy
|
|
||||||
|
|
||||||
## PR-Agent Pro 💎
|
## PR-Agent Pro 💎
|
||||||
|
|
||||||
@ -14,4 +13,4 @@ https://openai.com/enterprise-privacy
|
|||||||
|
|
||||||
## PR-Agent Chrome extension
|
## 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.
|
- The [PR-Agent Chrome extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) will not send your code to any external servers.
|
||||||
|
@ -1,18 +1,52 @@
|
|||||||
[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:
|
### Overview
|
||||||
|
|
||||||
|
[PR-Agent Pro](https://www.codium.ai/pricing/) is a hosted version of PR-Agent, provided by CodiumAI. A complimentary two-week trial is offered, followed by a monthly subscription fee.
|
||||||
|
PR-Agent 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 PR-Agent app to your GitHub\GitLab\BitBucket repo.
|
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.
|
|
||||||
|
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 and Claude accounts 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. It has the following additional tools and features:
|
|
||||||
- (Tool): [**Analyze PR components**](./tools/analyze.md/)
|
4. **Supporting self-hosted git servers** - PR-Agent Pro can be installed on GitHub Enterprise Server, GitLab, and BitBucket. For more information, see the [installation guide](https://pr-agent-docs.codium.ai/installation/pr_agent_pro/).
|
||||||
- (Tool): [**Custom Prompt Suggestions**](./tools/custom_prompt.md/)
|
|
||||||
- (Tool): [**Tests**](./tools/test.md/)
|
5. **PR Chat** - PR-Agent Pro allows you to engage in [private chat](https://pr-agent-docs.codium.ai/chrome-extension/features/#pr-chat) about your pull requests on private repositories.
|
||||||
- (Tool): [**PR documentation**](./tools/documentation.md/)
|
|
||||||
- (Tool): [**Improve Component**](https://pr-agent-docs.codium.ai/tools/improve_component/)
|
### Additional features
|
||||||
- (Tool): [**Similar code search**](https://pr-agent-docs.codium.ai/tools/similar_code/)
|
|
||||||
- (Tool): [**CI feedback**](./tools/ci_feedback.md/)
|
Here are some of the additional features and capabilities that PR-Agent Pro offers:
|
||||||
- (Feature): [**Interactive triggering**](./usage-guide/automations_and_usage.md/#interactive-triggering)
|
|
||||||
- (Feature): [**SOC2 compliance check**](./tools/review.md/#soc2-ticket-compliance)
|
| Feature | Description |
|
||||||
- (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)
|
| [**Model selection**](https://pr-agent-docs.codium.ai/usage-guide/PR_agent_pro_models/#pr-agent-pro-models) | Choose the model that best fits your needs, among top models like `GPT4` and `Claude-Sonnet-3.5`
|
||||||
- (Feature): [**Inline file summary**](https://pr-agent-docs.codium.ai/tools/describe/#inline-file-summary)
|
| [**Global and wiki configuration**](https://pr-agent-docs.codium.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://pr-agent-docs.codium.ai/tools/improve/#overview) | Generate commitable code from the relevant suggestions interactively by clicking on a checkbox |
|
||||||
|
| [**Suggestions impact**](https://pr-agent-docs.codium.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://pr-agent-docs.codium.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/#/) | PR-Agent Pro offers detailed statistics at user, repository, and company levels, including metrics about PR-Agent usage, and also general statistics and insights |
|
||||||
|
| [**Incorporating companies' best practices**](https://pr-agent-docs.codium.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://pr-agent-docs.codium.ai/tools/analyze/#example-usage) | Interactively apply different tools via the `analyze` command |
|
||||||
|
| [**SOC2 compliance check**](https://pr-agent-docs.codium.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://pr-agent-docs.codium.ai/tools/describe/#handle-custom-labels-from-the-repos-labels-page) | Define custom labels for PR-Agent to assign to the PR |
|
||||||
|
|
||||||
|
### Additional tools
|
||||||
|
|
||||||
|
Here are additional tools that are available only for PR-Agent Pro users:
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| [**Custom Prompt Suggestions**](https://pr-agent-docs.codium.ai/tools/custom_prompt/) | Generate code suggestions based on custom prompts from the user |
|
||||||
|
| [**Analyze PR components**](https://pr-agent-docs.codium.ai/tools/analyze/) | Identify the components that changed in the PR, and enable to interactively apply different tools to them |
|
||||||
|
| [**Tests**](https://pr-agent-docs.codium.ai/tools/test/) | Generate tests for code components that changed in the PR |
|
||||||
|
| [**PR documentation**](https://pr-agent-docs.codium.ai/tools/documentation/) | Generate docstring for code components that changed in the PR |
|
||||||
|
| [**Improve Component**](https://pr-agent-docs.codium.ai/tools/improve_component/) | Generate code suggestions for code components that changed in the PR |
|
||||||
|
| [**Similar code search**](https://pr-agent-docs.codium.ai/tools/similar_code/) | Search for similar code in the repository, organization, or entire GitHub |
|
||||||
|
|
||||||
|
|
||||||
|
### Supported languages
|
||||||
|
|
||||||
|
PR-Agent 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, PR-Agent Pro offers support for specific languages. For more details about features that require static code analysis, please refer to the [documentation](https://pr-agent-docs.codium.ai/tools/analyze/#overview).
|
@ -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:
|
||||||
```
|
```
|
||||||
|
@ -1,24 +1,29 @@
|
|||||||
## 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:
|
||||||
```
|
```
|
||||||
/improve
|
/improve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
Note that the `Apply this suggestion` checkbox, which interactively converts a suggestion into a commitable code comment, is available only for PR-Agent 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:
|
||||||
|
|
||||||
{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:
|
||||||
```
|
```
|
||||||
/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:
|
||||||
```
|
```
|
||||||
/improve --pr_code_suggestions.commitable_code_suggestions=true
|
/improve --pr_code_suggestions.commitable_code_suggestions=true
|
||||||
```
|
```
|
||||||
@ -26,8 +31,8 @@ For example, you can choose to present the suggestions as commitable code commen
|
|||||||
{width=512}
|
{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
|
||||||
|
|
||||||
@ -47,17 +52,23 @@ 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 PR-Agent 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, PR-Agent will utilize, after each commit, a dedicated logic to identify if a suggestion was implemented, and will mark it as implemented.
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
In post-process, PR-Agent counts the number of suggestions that were implemented, and provides general statistics and insights about the suggestions' impact on the PR process.
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Tips
|
||||||
|
|
||||||
### Self-review
|
### Self-review
|
||||||
If you set in a configuration file:
|
If you set in a configuration file:
|
||||||
@ -71,8 +82,10 @@ You can set the content of the checkbox text via:
|
|||||||
[pr_code_suggestions]
|
[pr_code_suggestions]
|
||||||
code_suggestions_self_review_text = "... (your text here) ..."
|
code_suggestions_self_review_text = "... (your text here) ..."
|
||||||
```
|
```
|
||||||
|
|
||||||
{width=512}
|
{width=512}
|
||||||
|
|
||||||
|
|
||||||
💎 In addition, by setting:
|
💎 In addition, by setting:
|
||||||
```
|
```
|
||||||
[pr_code_suggestions]
|
[pr_code_suggestions]
|
||||||
@ -80,14 +93,81 @@ approve_pr_on_self_review = true
|
|||||||
```
|
```
|
||||||
the tool can automatically approve the PR when the user checks the self-review checkbox.
|
the tool can automatically approve the PR when the user checks the self-review checkbox.
|
||||||
|
|
||||||
!!! tip "Demanding self-review from the PR author"
|
!!! tip "Tip - demanding self-review from the PR author"
|
||||||
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).
|
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).
|
||||||
|
|
||||||
{width=512}
|
{width=512}
|
||||||
|
|
||||||
|
### '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:
|
||||||
|
```
|
||||||
|
[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. Here’s 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, PR-Agent 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:
|
||||||
|
|
||||||
|
```
|
||||||
|
[best_practices]
|
||||||
|
enable_global_best_practices = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, create a `best_practices.md` wiki file in the root of [global](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/#global-configuration-file) configuration repository, `pr-agent-settings`.
|
||||||
|
|
||||||
|
##### Example results
|
||||||
|
|
||||||
|
{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
|
## Configuration options
|
||||||
|
|
||||||
!!! example "General options"
|
??? example "General options"
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -126,9 +206,13 @@ the tool can automatically approve the PR when the user checks the self-review c
|
|||||||
<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 true.</td>
|
<td>If set to true, the tool will display a help text in the comment. Default is true.</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
|
|
||||||
!!! example "params for 'extended' mode"
|
??? example "params for 'extended' mode"
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -153,43 +237,14 @@ the tool can automatically approve the PR when the user checks the self-review c
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Usage Tips
|
|
||||||
|
|
||||||
!!! tip "Extra instructions"
|
|
||||||
|
|
||||||
Extra instructions are very important for the `improve` tool, since they enable you to guide the model to suggestions that are more relevant to the specific needs of the project.
|
|
||||||
|
|
||||||
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify relevant aspects that you want the model to focus on.
|
|
||||||
|
|
||||||
Examples for extra instructions:
|
|
||||||
```
|
|
||||||
[pr_code_suggestions] # /improve #
|
|
||||||
extra_instructions="""\
|
|
||||||
Emphasize the following aspects:
|
|
||||||
- Does the code logic cover relevant edge cases?
|
|
||||||
- Is the code logic clear and easy to understand?
|
|
||||||
- Is the code logic efficient?
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
|
|
||||||
|
|
||||||
!!! 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.
|
|
||||||
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)).
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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://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.
|
||||||
- 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 `extra_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://pr-agent-docs.codium.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://pr-agent-docs.codium.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.
|
|
||||||
|
@ -8,6 +8,9 @@ The tool can be triggered automatically every time a new PR is [opened](../usage
|
|||||||
|
|
||||||
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.
|
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 PR-Agent 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
|
||||||
@ -43,15 +46,23 @@ 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.
|
|
||||||
|
|
||||||
{width=512}
|
[//]: # (Incremental review only considers changes since the last PR-Agent review. This can be useful when working on the PR in an iterative manner, and you want to focus on the changes since the last review instead of reviewing the entire PR again.)
|
||||||
|
|
||||||
|
[//]: # (For invoking the incremental mode, the following command can be used:)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (/review -i)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (Note that the incremental mode is only available for GitHub.)
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # ({width=512})
|
||||||
|
|
||||||
[//]: # (### PR Reflection)
|
[//]: # (### PR Reflection)
|
||||||
|
|
||||||
@ -84,11 +95,11 @@ Note that the incremental mode is only available for GitHub.
|
|||||||
<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>
|
||||||
@ -166,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>
|
||||||
@ -192,7 +203,7 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
|
|||||||
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 "Possible labels from the review tool"
|
!!! tip "Possible labels from the review tool"
|
||||||
|
|
||||||
@ -210,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="""\
|
||||||
|
@ -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:
|
||||||
|
|
||||||
{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):
|
||||||
|
|
||||||
{width=768}
|
{width=768}
|
||||||
|
|
||||||
{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)):
|
||||||
|
|
||||||
|
189
docs/docs/usage-guide/EXAMPLE_BEST_PRACTICE.md
Normal file
189
docs/docs/usage-guide/EXAMPLE_BEST_PRACTICE.md
Normal 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 function’s 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 Python’s built-in data types such as dictionaries appear to have atomic operations, there are corner cases where they aren’t 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` module’s `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.
|
30
docs/docs/usage-guide/PR_agent_pro_models.md
Normal file
30
docs/docs/usage-guide/PR_agent_pro_models.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
## PR-Agent Pro Models
|
||||||
|
|
||||||
|
The default models used by PR-Agent Pro are OpenAI's GPT-4 models. We use a combination of GPT-4-Turbo and GPT-4o to strike a balance between speed and quality.
|
||||||
|
|
||||||
|
However, users can change the model used by PR-Agent Pro to Claude-3.5-sonnet, which also excels at code tasks.
|
||||||
|
To do so, add the following to your [configuration](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[config]
|
||||||
|
model="claude-3-5-sonnet"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that Claude models tend to give lower scores for each suggestion, so if you are using a [threshold](https://pr-agent-docs.codium.ai/tools/improve/#configuration-options):
|
||||||
|
```
|
||||||
|
[pr_code_suggestions]
|
||||||
|
suggestions_score_threshold=...
|
||||||
|
```
|
||||||
|
You might need to adjust this value when switching models.
|
||||||
|
|
||||||
|
### Dedicated models per tool
|
||||||
|
|
||||||
|
You can also use different models for different tools. For example, you can use the Claude-3.5-sonnet model only for the `improve` tool (and keep the default GPT-4 model for the other tools) by adding the following to your configuration file:
|
||||||
|
```
|
||||||
|
[github_app]
|
||||||
|
pr_commands = [
|
||||||
|
"/describe --pr_description.final_update_message=false",
|
||||||
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
|
"/improve --config.model=claude-3-5-sonnet",
|
||||||
|
]
|
||||||
|
```
|
@ -1,6 +1,28 @@
|
|||||||
|
## Show possible configurations
|
||||||
|
The possible configurations of pr-agent are stored in [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml).
|
||||||
|
In the [tools](https://pr-agent-docs.codium.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
|
||||||
|
```
|
||||||
|
|
||||||
|
{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.
|
||||||
|
|
||||||
|
{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 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 vendor code.
|
||||||
|
|
||||||
You can ignore files or folders using the following methods:
|
You can ignore files or folders using the following methods:
|
||||||
- `IGNORE.GLOB`
|
- `IGNORE.GLOB`
|
||||||
@ -44,171 +66,9 @@ When the PR is above the token limit, it employs a [PR Compression strategy](../
|
|||||||
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://codium-ai.github.io/Docs-PR-Agent/usage-guide/#changing-a-model) with larger context, like GPT-32K, or claude-100K. This solution will be applicable for all the tools.
|
||||||
2) For the `/improve` tool, there is an ['extended' mode](https://codium-ai.github.io/Docs-PR-Agent/tools/#improve) (`/improve --extended`),
|
2) For the `/improve` tool, there is an ['extended' mode](https://codium-ai.github.io/Docs-PR-Agent/tools/#improve) (`/improve --extended`),
|
||||||
which divides the PR to chunks, and 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)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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"
|
|
||||||
|
|
||||||
[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"
|
|
||||||
|
|
||||||
[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"
|
|
||||||
[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
|
||||||
|
|
||||||
@ -225,14 +85,16 @@ 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:
|
PR-Agent 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)), PR-Agent may automatically set this number to 0, and will use the original git patch.
|
||||||
|
|
||||||
|
|
||||||
## Editing the prompts
|
## Editing the prompts
|
||||||
@ -252,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 . PR-Agent 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.
|
||||||
|
@ -26,6 +26,16 @@ 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 PR-Agent. Currently, the following providers are supported:
|
||||||
|
`
|
||||||
|
"github", "gitlab", "bitbucket", "azure", "codecommit", "local", "gerrit"
|
||||||
|
`
|
||||||
|
|
||||||
|
Default is "github".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Online usage
|
### Online usage
|
||||||
|
|
||||||
@ -34,7 +44,7 @@ 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`
|
||||||
@ -84,13 +94,6 @@ To cancel the automatic run of all the tools, set:
|
|||||||
pr_commands = []
|
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.
|
||||||
@ -118,10 +121,14 @@ 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.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).
|
||||||
@ -173,6 +180,12 @@ inline_code_comments = true
|
|||||||
|
|
||||||
Each time you invoke a `/review` tool, it will use inline code comments.
|
Each time you invoke a `/review` tool, it will use inline code comments.
|
||||||
|
|
||||||
|
|
||||||
|
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 PR-Agent, you might want to set: `bitbucket_app.avoid_full_files=true` in your configuration file.
|
||||||
|
This will prevent PR-Agent 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
|
||||||
|
|
||||||
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file:
|
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file:
|
||||||
|
189
docs/docs/usage-guide/changing_a_model.md
Normal file
189
docs/docs/usage-guide/changing_a_model.md
Normal 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.
|
@ -18,23 +18,29 @@ In terms of precedence, wiki configurations will override local configurations,
|
|||||||
|
|
||||||
## Wiki configuration file 💎
|
## Wiki configuration file 💎
|
||||||
|
|
||||||
For GitHub and GitLab, 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 PR-Agent-Pro, you can set configurations by creating a page called `.pr_agent.toml` in the [wiki](https://github.com/Codium-ai/pr-agent/wiki/pr_agent.toml) of the repo.
|
||||||
The advantage of this method is that it allows to set configurations without needing to commit new content to the repo - just edit the wiki page and **save**.
|
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**.
|
||||||
|
|
||||||
|
|
||||||
{width=512}
|
{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.
|
PR-Agent 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`.
|
||||||
|
@ -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 PR-Agent.
|
||||||
|
It includes information on how to adjust PR-Agent 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,6 +15,7 @@ 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)
|
||||||
@ -21,3 +23,4 @@ This page provides a detailed guide on how to use PR-Agent. It includes informat
|
|||||||
- [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)
|
||||||
|
- [PR-Agent Pro Models](./PR_agent_pro_models.md)
|
@ -7,12 +7,7 @@ After [installation](https://pr-agent-docs.codium.ai/installation/), there are t
|
|||||||
|
|
||||||
|
|
||||||
Specifically, CLI commands can be issued by invoking a pre-built [docker image](https://pr-agent-docs.codium.ai/installation/locally/#using-docker-image), or by invoking a [locally cloned repo](https://pr-agent-docs.codium.ai/installation/locally/#run-from-source).
|
Specifically, CLI commands can be issued by invoking a pre-built [docker image](https://pr-agent-docs.codium.ai/installation/locally/#using-docker-image), or by invoking a [locally cloned repo](https://pr-agent-docs.codium.ai/installation/locally/#run-from-source).
|
||||||
For online usage, you will need to setup either a [GitHub App](https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-app), or a [GitHub Action](https://pr-agent-docs.codium.ai/installation/github/#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://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-app) or a [GitHub Action](https://pr-agent-docs.codium.ai/installation/github/#run-as-a-github-action) (GitHub), a [GitLab webhook](https://pr-agent-docs.codium.ai/installation/gitlab/#run-a-gitlab-webhook-server) (GitLab), or a [BitBucket App](https://pr-agent-docs.codium.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#L5) 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 PR-Agent specific tools automatically when a new PR is opened, or on each push to a branch.
|
||||||
`
|
|
||||||
"github", "gitlab", "bitbucket", "azure", "codecommit", "local", "gerrit"
|
|
||||||
`
|
|
||||||
|
|
||||||
|
@ -18,10 +18,12 @@ nav:
|
|||||||
- 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'
|
||||||
|
- 💎 PR-Agent Pro Models: 'usage-guide/PR_agent_pro_models'
|
||||||
- Tools:
|
- Tools:
|
||||||
- 'tools/index.md'
|
- 'tools/index.md'
|
||||||
- Describe: 'tools/describe.md'
|
- Describe: 'tools/describe.md'
|
||||||
@ -39,9 +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'
|
- Code Fine-tuning Benchmark: 'finetuning_benchmark/index.md'
|
||||||
|
- Chrome Extension:
|
||||||
|
- PR-Agent 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
|
||||||
@ -131,7 +148,7 @@ markdown_extensions:
|
|||||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||||
- toc:
|
- toc:
|
||||||
title: On this page
|
title: On this page
|
||||||
toc_depth: 2
|
toc_depth: 3
|
||||||
permalink: true
|
permalink: true
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,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
|
||||||
|
@ -16,6 +16,13 @@ MAX_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,
|
||||||
@ -28,6 +35,8 @@ MAX_TOKENS = {
|
|||||||
'vertex_ai/claude-3-opus@20240229': 100000,
|
'vertex_ai/claude-3-opus@20240229': 100000,
|
||||||
'vertex_ai/claude-3-5-sonnet@20240620': 100000,
|
'vertex_ai/claude-3-5-sonnet@20240620': 100000,
|
||||||
'vertex_ai/gemini-1.5-pro': 1048576,
|
'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,
|
||||||
@ -41,7 +50,18 @@ MAX_TOKENS = {
|
|||||||
'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,
|
'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,
|
'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,
|
||||||
}
|
}
|
||||||
|
@ -20,35 +20,13 @@ class LangChainOpenAIHandler(BaseAiHandler):
|
|||||||
# 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(
|
|
||||||
lambda **kwargs: AzureChatOpenAI(**kwargs),
|
|
||||||
openai_api_key=get_settings().openai.key,
|
|
||||||
openai_api_base=get_settings().openai.api_base,
|
|
||||||
openai_api_version=get_settings().openai.api_version,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 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:
|
|
||||||
self._chat = ChatOpenAI(openai_api_key=get_settings().openai.key)
|
|
||||||
else:
|
|
||||||
self._chat = 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
|
|
||||||
|
|
||||||
def chat(self, messages: list, model: str, temperature: float):
|
def chat(self, messages: list, model: str, temperature: float):
|
||||||
if self.azure:
|
chat = self._create_chat(self.deployment_id)
|
||||||
# we must set the deployment_id only here (instead of the __init__ method) to support fallback_deployments
|
return chat.invoke(input=messages, model=model, temperature=temperature)
|
||||||
return self._chat.invoke(input = messages, model=model, temperature=temperature, deployment_name=self.deployment_id)
|
|
||||||
else:
|
|
||||||
return self._chat.invoke(input = messages, model=model, temperature=temperature)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def deployment_id(self):
|
def deployment_id(self):
|
||||||
@ -71,3 +49,28 @@ class LangChainOpenAIHandler(BaseAiHandler):
|
|||||||
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -44,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):
|
||||||
@ -89,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):
|
||||||
"""
|
"""
|
||||||
@ -106,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:
|
||||||
@ -126,9 +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 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
|
||||||
|
|
||||||
@ -140,13 +215,13 @@ class LiteLLMAIHandler(BaseAiHandler):
|
|||||||
|
|
||||||
response = await acompletion(**kwargs)
|
response = await acompletion(**kwargs)
|
||||||
except (openai.APIError, openai.APITimeoutError) 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
|
||||||
|
@ -33,9 +33,29 @@ def filter_ignored(files, platform = 'github'):
|
|||||||
if platform == 'github':
|
if platform == 'github':
|
||||||
files = [f for f in files if (f.filename and not r.match(f.filename))]
|
files = [f for f in files if (f.filename and not r.match(f.filename))]
|
||||||
elif platform == 'bitbucket':
|
elif platform == 'bitbucket':
|
||||||
files = [f for f in files if (f.new.path and not r.match(f.new.path))]
|
# 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':
|
elif platform == 'gitlab':
|
||||||
files = [f for f in files if (f['new_path'] and not r.match(f['new_path']))]
|
# 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':
|
elif platform == 'azure':
|
||||||
files = [f for f in files if not r.match(f)]
|
files = [f for f in files if not r.match(f)]
|
||||||
|
|
||||||
|
@ -1,34 +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:
|
||||||
|
return patch_str
|
||||||
Args:
|
|
||||||
original_file_str (str): The original file to which the patch will be applied.
|
original_file_str = decode_if_bytes(original_file_str)
|
||||||
patch_str (str): The patch to be applied to the original file.
|
if not original_file_str:
|
||||||
num_lines (int): The number of surrounding lines to include in the extended patch.
|
return patch_str
|
||||||
|
|
||||||
Returns:
|
if should_skip_patch(filename):
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
original_file_str = original_file_str.decode('utf-8')
|
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:
|
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 ""
|
||||||
|
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 = []
|
||||||
|
|
||||||
@ -39,12 +68,87 @@ 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)
|
||||||
|
|
||||||
|
section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
|
||||||
|
|
||||||
|
if patch_extra_lines_before > 0 or patch_extra_lines_after > 0:
|
||||||
|
def _calc_context_limits(patch_lines_before):
|
||||||
|
extended_start1 = max(1, start1 - patch_lines_before)
|
||||||
|
extended_size1 = size1 + (start1 - extended_start1) + patch_extra_lines_after
|
||||||
|
extended_start2 = max(1, start2 - patch_lines_before)
|
||||||
|
extended_size2 = size2 + (start2 - extended_start2) + patch_extra_lines_after
|
||||||
|
if extended_start1 - 1 + extended_size1 > len_original_lines:
|
||||||
|
# we cannot extend beyond the original file
|
||||||
|
delta_cap = extended_start1 - 1 + extended_size1 - len_original_lines
|
||||||
|
extended_size1 = max(extended_size1 - delta_cap, size1)
|
||||||
|
extended_size2 = max(extended_size2 - delta_cap, size2)
|
||||||
|
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(
|
||||||
|
f'@@ -{extended_start1},{extended_size1} '
|
||||||
|
f'+{extended_start2},{extended_size2} @@ {section_header}')
|
||||||
|
extended_patch_lines.extend(delta_lines) # one to zero based
|
||||||
|
continue
|
||||||
|
extended_patch_lines.append(line)
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().warning(f"Failed to extend patch: {e}", artifact={"traceback": traceback.format_exc()})
|
||||||
|
return patch_str
|
||||||
|
|
||||||
|
# finish processing last hunk
|
||||||
|
if start1 != -1 and patch_extra_lines_after > 0:
|
||||||
|
delta_lines = original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]
|
||||||
|
# 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)
|
||||||
|
return extended_patch_str
|
||||||
|
|
||||||
|
|
||||||
|
def extract_hunk_headers(match):
|
||||||
res = list(match.groups())
|
res = list(match.groups())
|
||||||
for i in range(len(res)):
|
for i in range(len(res)):
|
||||||
if res[i] is None:
|
if res[i] is None:
|
||||||
@ -55,29 +159,7 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str:
|
|||||||
start1, size1, size2 = map(int, res[:3])
|
start1, size1, size2 = map(int, res[:3])
|
||||||
start2 = 0
|
start2 = 0
|
||||||
section_header = res[4]
|
section_header = res[4]
|
||||||
extended_start1 = max(1, start1 - num_lines)
|
return section_header, size1, size2, start1, start2
|
||||||
extended_size1 = size1 + (start1 - extended_start1) + num_lines
|
|
||||||
extended_start2 = max(1, start2 - num_lines)
|
|
||||||
extended_size2 = size2 + (start2 - extended_start2) + num_lines
|
|
||||||
extended_patch_lines.append(
|
|
||||||
f'@@ -{extended_start1},{extended_size1} '
|
|
||||||
f'+{extended_start2},{extended_size2} @@ {section_header}')
|
|
||||||
extended_patch_lines.extend(
|
|
||||||
original_lines[extended_start1 - 1:start1 - 1]) # one to zero based
|
|
||||||
continue
|
|
||||||
extended_patch_lines.append(line)
|
|
||||||
except Exception as e:
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().error(f"Failed to extend patch: {e}")
|
|
||||||
return patch_str
|
|
||||||
|
|
||||||
# finish previous hunk
|
|
||||||
if start1 != -1:
|
|
||||||
extended_patch_lines.extend(
|
|
||||||
original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines])
|
|
||||||
|
|
||||||
extended_patch_str = '\n'.join(extended_patch_lines)
|
|
||||||
return extended_patch_str
|
|
||||||
|
|
||||||
|
|
||||||
def omit_deletion_hunks(patch_lines) -> str:
|
def omit_deletion_hunks(patch_lines) -> str:
|
||||||
@ -109,6 +191,7 @@ def omit_deletion_hunks(patch_lines) -> str:
|
|||||||
inside_hunk = True
|
inside_hunk = True
|
||||||
else:
|
else:
|
||||||
temp_hunk.append(line)
|
temp_hunk.append(line)
|
||||||
|
if line:
|
||||||
edit_type = line[0]
|
edit_type = line[0]
|
||||||
if edit_type == '+':
|
if edit_type == '+':
|
||||||
add_hunk = True
|
add_hunk = True
|
||||||
@ -183,8 +266,11 @@ __old hunk__
|
|||||||
line6
|
line6
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
|
# if the file was deleted, return a message indicating that the file was deleted
|
||||||
|
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_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+))? @@[ ]?(.*)")
|
||||||
@ -194,21 +280,25 @@ __old hunk__
|
|||||||
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 new_content_lines:
|
|
||||||
if prev_header_line:
|
if prev_header_line:
|
||||||
patch_with_lines_str += f'\n{prev_header_line}\n'
|
patch_with_lines_str += f'\n{prev_header_line}\n'
|
||||||
|
if new_content_lines:
|
||||||
|
is_plus_lines = any([line.startswith('+') for line in new_content_lines])
|
||||||
|
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:
|
||||||
|
is_minus_lines = any([line.startswith('-') for line in old_content_lines])
|
||||||
|
if is_minus_lines:
|
||||||
patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
|
patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
|
||||||
for line_old in old_content_lines:
|
for line_old in old_content_lines:
|
||||||
patch_with_lines_str += f"{line_old}\n"
|
patch_with_lines_str += f"{line_old}\n"
|
||||||
@ -217,32 +307,33 @@ __old hunk__
|
|||||||
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:
|
||||||
if new_content_lines:
|
|
||||||
patch_with_lines_str += f'\n{header_line}\n'
|
patch_with_lines_str += f'\n{header_line}\n'
|
||||||
|
if new_content_lines:
|
||||||
|
is_plus_lines = any([line.startswith('+') for line in new_content_lines])
|
||||||
|
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:
|
||||||
|
is_minus_lines = any([line.startswith('-') for line in old_content_lines])
|
||||||
|
if is_minus_lines:
|
||||||
patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
|
patch_with_lines_str = patch_with_lines_str.rstrip() + '\n__old hunk__\n'
|
||||||
for line_old in old_content_lines:
|
for line_old in old_content_lines:
|
||||||
patch_with_lines_str += f"{line_old}\n"
|
patch_with_lines_str += f"{line_old}\n"
|
||||||
@ -252,7 +343,7 @@ __old hunk__
|
|||||||
|
|
||||||
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(
|
||||||
@ -272,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':
|
||||||
|
@ -14,7 +14,9 @@ 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)]
|
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, bad_extensions=None):
|
def is_valid_file(filename:str, bad_extensions=None) -> bool:
|
||||||
|
if not filename:
|
||||||
|
return False
|
||||||
if not bad_extensions:
|
if not bad_extensions:
|
||||||
bad_extensions = get_settings().bad_extensions.default
|
bad_extensions = get_settings().bad_extensions.default
|
||||||
if get_settings().config.use_extra_bad_extensions:
|
if get_settings().config.use_extra_bad_extensions:
|
||||||
|
@ -23,8 +23,15 @@ ADDED_FILES_ = "Additional added files (insufficient token budget to process):\n
|
|||||||
|
|
||||||
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1500
|
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1500
|
||||||
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 1000
|
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 1000
|
||||||
|
MAX_EXTRA_LINES = 10
|
||||||
|
|
||||||
|
|
||||||
|
def cap_and_log_extra_lines(value, direction) -> int:
|
||||||
|
if value > MAX_EXTRA_LINES:
|
||||||
|
get_logger().warning(f"patch_extra_lines_{direction} was {value}, capping to {MAX_EXTRA_LINES}")
|
||||||
|
return MAX_EXTRA_LINES
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
||||||
model: str,
|
model: str,
|
||||||
@ -33,9 +40,13 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
|||||||
large_pr_handling=False,
|
large_pr_handling=False,
|
||||||
return_remaining_files=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()
|
||||||
@ -64,7 +75,8 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
|||||||
|
|
||||||
# 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):
|
||||||
@ -72,7 +84,7 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
|||||||
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_list, total_tokens_list, deleted_files_list, remaining_files_list, file_dict, files_in_patches_list = \
|
patches_compressed_list, total_tokens_list, deleted_files_list, remaining_files_list, file_dict, files_in_patches_list = \
|
||||||
@ -80,7 +92,7 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
|||||||
|
|
||||||
if large_pr_handling and len(patches_compressed_list) > 1:
|
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.")
|
get_logger().info(f"Large PR handling mode, and found {len(patches_compressed_list)} patches with original diff.")
|
||||||
return "" # return empty string, as we generate multiple patches with a different prompt
|
return "" # return empty string, as we want to generate multiple patches with a different prompt
|
||||||
|
|
||||||
# return the first patch
|
# return the first patch
|
||||||
patches_compressed = patches_compressed_list[0]
|
patches_compressed = patches_compressed_list[0]
|
||||||
@ -105,7 +117,7 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler,
|
|||||||
added_list_str = ADDED_FILES_ + f"\n{filename}"
|
added_list_str = ADDED_FILES_ + f"\n{filename}"
|
||||||
else:
|
else:
|
||||||
added_list_str = added_list_str + f"\n{filename}"
|
added_list_str = added_list_str + f"\n{filename}"
|
||||||
elif file_values['edit_type'] == EDIT_TYPE.MODIFIED or EDIT_TYPE.RENAMED:
|
elif file_values['edit_type'] in [EDIT_TYPE.MODIFIED, EDIT_TYPE.RENAMED]:
|
||||||
unprocessed_files.append(filename)
|
unprocessed_files.append(filename)
|
||||||
if not modified_list_str:
|
if not modified_list_str:
|
||||||
modified_list_str = MORE_MODIFIED_FILES_ + f"\n{filename}"
|
modified_list_str = MORE_MODIFIED_FILES_ + f"\n{filename}"
|
||||||
@ -174,17 +186,8 @@ def get_pr_diff_multiple_patchs(git_provider: GitProvider, token_handler: TokenH
|
|||||||
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 = []
|
||||||
@ -196,15 +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,
|
||||||
|
patch_extra_lines_before, patch_extra_lines_after, file.filename)
|
||||||
if not extended_patch:
|
if not extended_patch:
|
||||||
get_logger().warning(f"Failed to extend patch for file: {file.filename}")
|
get_logger().warning(f"Failed to extend patch for file: {file.filename}")
|
||||||
continue
|
continue
|
||||||
full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n"
|
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
|
||||||
@ -244,6 +252,10 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
|
|||||||
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}
|
file_dict[file.filename] = {'patch': patch, 'tokens': new_patch_tokens, 'edit_type': file.edit_type}
|
||||||
|
|
||||||
@ -309,7 +321,7 @@ def generate_full_patch(convert_hunks_to_line_numbers, file_dict, max_tokens_mod
|
|||||||
|
|
||||||
if patch:
|
if patch:
|
||||||
if not convert_hunks_to_line_numbers:
|
if not convert_hunks_to_line_numbers:
|
||||||
patch_final = f"\n\n## 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)
|
||||||
@ -335,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]:
|
||||||
@ -405,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 = []
|
||||||
@ -434,6 +453,9 @@ 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(
|
if patch and (token_handler.prompt_tokens + new_patch_tokens) > get_max_tokens(
|
||||||
@ -462,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}")
|
||||||
|
|
||||||
@ -477,3 +503,46 @@ def get_pr_multi_diffs(git_provider: GitProvider,
|
|||||||
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
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
system_prompt = environment.from_string(system).render(vars)
|
system_prompt = environment.from_string(system).render(vars)
|
||||||
user_prompt = environment.from_string(user).render(vars)
|
user_prompt = environment.from_string(user).render(vars)
|
||||||
system_prompt_tokens = len(encoder.encode(system_prompt))
|
system_prompt_tokens = len(encoder.encode(system_prompt))
|
||||||
user_prompt_tokens = len(encoder.encode(user_prompt))
|
user_prompt_tokens = len(encoder.encode(user_prompt))
|
||||||
return system_prompt_tokens + user_prompt_tokens
|
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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
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
|
||||||
@ -11,6 +14,7 @@ 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
|
||||||
@ -19,6 +23,12 @@ 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"
|
||||||
@ -37,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, only_markdown=False) -> 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(": ")
|
||||||
@ -46,7 +56,13 @@ def emphasize_header(text: str, only_markdown=False) -> str:
|
|||||||
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
|
||||||
if only_markdown:
|
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:]
|
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:
|
else:
|
||||||
transformed_string = "<strong>" + text[:colon_position + 1] + "</strong>" +'<br>' + text[colon_position + 1:]
|
transformed_string = "<strong>" + text[:colon_position + 1] + "</strong>" +'<br>' + text[colon_position + 1:]
|
||||||
else:
|
else:
|
||||||
@ -70,7 +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, gfm_supported: bool = True, incremental_review=None) -> str:
|
def convert_to_markdown_v2(output_data: dict,
|
||||||
|
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:
|
||||||
@ -106,12 +125,13 @@ def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, increm
|
|||||||
|
|
||||||
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 'Estimated effort to review' in key_nice:
|
if 'Estimated effort to review' in key_nice:
|
||||||
key_nice = 'Estimated effort to review'
|
key_nice = 'Estimated effort to review'
|
||||||
|
value = str(value).strip()
|
||||||
if value.isnumeric():
|
if value.isnumeric():
|
||||||
value_int = int(value)
|
value_int = int(value)
|
||||||
else:
|
else:
|
||||||
@ -129,7 +149,7 @@ def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, increm
|
|||||||
else:
|
else:
|
||||||
markdown_text += f"### {emoji} {key_nice}: {value}\n\n"
|
markdown_text += f"### {emoji} {key_nice}: {value}\n\n"
|
||||||
elif 'relevant tests' in key_nice.lower():
|
elif 'relevant tests' in key_nice.lower():
|
||||||
value = value.strip().lower()
|
value = str(value).strip().lower()
|
||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
markdown_text += f"<tr><td>"
|
markdown_text += f"<tr><td>"
|
||||||
if is_value_no(value):
|
if is_value_no(value):
|
||||||
@ -137,13 +157,6 @@ def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, increm
|
|||||||
else:
|
else:
|
||||||
markdown_text += f"{emoji} <strong>PR contains tests</strong>"
|
markdown_text += f"{emoji} <strong>PR contains tests</strong>"
|
||||||
markdown_text += f"</td></tr>\n"
|
markdown_text += f"</td></tr>\n"
|
||||||
else:
|
|
||||||
if gfm_supported:
|
|
||||||
markdown_text += f"<tr><td>"
|
|
||||||
if is_value_no(value):
|
|
||||||
markdown_text += f"{emoji} <strong>No relevant tests</strong>"
|
|
||||||
else:
|
|
||||||
markdown_text += f"{emoji} <strong>PR contains tests</strong>"
|
|
||||||
else:
|
else:
|
||||||
if is_value_no(value):
|
if is_value_no(value):
|
||||||
markdown_text += f'### {emoji} No relevant tests\n\n'
|
markdown_text += f'### {emoji} No relevant tests\n\n'
|
||||||
@ -164,7 +177,7 @@ def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, increm
|
|||||||
markdown_text += f'### {emoji} No security concerns identified\n\n'
|
markdown_text += f'### {emoji} No security concerns identified\n\n'
|
||||||
else:
|
else:
|
||||||
markdown_text += f"### {emoji} Security concerns\n\n"
|
markdown_text += f"### {emoji} Security concerns\n\n"
|
||||||
value = emphasize_header(value.strip())
|
value = emphasize_header(value.strip(), only_markdown=True)
|
||||||
markdown_text += f"{value}\n\n"
|
markdown_text += f"{value}\n\n"
|
||||||
elif 'can be split' in key_nice.lower():
|
elif 'can be split' in key_nice.lower():
|
||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
@ -172,7 +185,7 @@ def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, increm
|
|||||||
markdown_text += process_can_be_split(emoji, value)
|
markdown_text += process_can_be_split(emoji, value)
|
||||||
markdown_text += f"</td></tr>\n"
|
markdown_text += f"</td></tr>\n"
|
||||||
elif 'key issues to review' in key_nice.lower():
|
elif 'key issues to review' in key_nice.lower():
|
||||||
value = value.strip()
|
# value is a list of issues
|
||||||
if is_value_no(value):
|
if is_value_no(value):
|
||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
markdown_text += f"<tr><td>"
|
markdown_text += f"<tr><td>"
|
||||||
@ -181,20 +194,33 @@ def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, increm
|
|||||||
else:
|
else:
|
||||||
markdown_text += f"### {emoji} No key issues to review\n\n"
|
markdown_text += f"### {emoji} No key issues to review\n\n"
|
||||||
else:
|
else:
|
||||||
issues = value.split('\n- ')
|
# issues = value.split('\n- ')
|
||||||
for i, _ in enumerate(issues):
|
issues =value
|
||||||
issues[i] = issues[i].strip().strip('-').strip()
|
# for i, _ in enumerate(issues):
|
||||||
issues = unique_strings(issues) # remove duplicates
|
# issues[i] = issues[i].strip().strip('-').strip()
|
||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
markdown_text += f"<tr><td>"
|
markdown_text += f"<tr><td>"
|
||||||
markdown_text += f"{emoji} <strong>{key_nice}</strong><br><br>\n\n"
|
markdown_text += f"{emoji} <strong>{key_nice}</strong><br><br>\n\n"
|
||||||
else:
|
else:
|
||||||
markdown_text += f"### {emoji} Key issues to review:\n\n"
|
markdown_text += f"### {emoji} Key issues to review\n\n#### \n"
|
||||||
for i, issue in enumerate(issues):
|
for i, issue in enumerate(issues):
|
||||||
|
try:
|
||||||
if not issue:
|
if not issue:
|
||||||
continue
|
continue
|
||||||
issue = emphasize_header(issue, only_markdown=True)
|
relevant_file = issue.get('relevant_file', '').strip()
|
||||||
markdown_text += f"{issue}\n\n"
|
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 key issues to review: {e}")
|
||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
markdown_text += f"</td></tr>\n"
|
markdown_text += f"</td></tr>\n"
|
||||||
else:
|
else:
|
||||||
@ -520,15 +546,21 @@ def _fix_key_value(key: str, value: str):
|
|||||||
|
|
||||||
|
|
||||||
def load_yaml(response_text: str, keys_fix_yaml: List[str] = [], first_key="", last_key="") -> 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, first_key=first_key, last_key=last_key)
|
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,
|
def try_fix_yaml(response_text: str,
|
||||||
keys_fix_yaml: List[str] = [],
|
keys_fix_yaml: List[str] = [],
|
||||||
first_key="",
|
first_key="",
|
||||||
@ -541,9 +573,9 @@ def try_fix_yaml(response_text: str,
|
|||||||
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_yaml:
|
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")
|
||||||
@ -551,14 +583,14 @@ def try_fix_yaml(response_text: str,
|
|||||||
except:
|
except:
|
||||||
get_logger().info(f"Failed to parse AI prediction after adding |-\n")
|
get_logger().info(f"Failed to parse AI prediction after adding |-\n")
|
||||||
|
|
||||||
# second fallback - try to extract only range from first ```yaml to ```
|
# second fallback - try to extract only range from first ```yaml to ````
|
||||||
snippet_pattern = r'```(yaml)?[\s\S]*?```'
|
snippet_pattern = r'```(yaml)?[\s\S]*?```'
|
||||||
snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
|
snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
|
||||||
if snippet:
|
if snippet:
|
||||||
snippet_text = snippet.group()
|
snippet_text = snippet.group()
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(snippet_text.removeprefix('```yaml').rstrip('`'))
|
data = yaml.safe_load(snippet_text.removeprefix('```yaml').rstrip('`'))
|
||||||
get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet with second fallback")
|
get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet")
|
||||||
return data
|
return data
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@ -573,6 +605,7 @@ def try_fix_yaml(response_text: str,
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# forth fallback - try to extract yaml snippet by 'first_key' and 'last_key'
|
# 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.
|
# 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
|
# it just needs to be some inner key, so we can look for newlines after it
|
||||||
@ -635,14 +668,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:
|
||||||
@ -654,15 +689,25 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@ -714,6 +759,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>'
|
||||||
@ -730,6 +776,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
|
||||||
@ -856,21 +905,24 @@ 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```"
|
||||||
@ -878,9 +930,79 @@ def show_relevant_configurations(relevant_section: str) -> str:
|
|||||||
return markdown_text
|
return markdown_text
|
||||||
|
|
||||||
def is_value_no(value):
|
def is_value_no(value):
|
||||||
if value is None:
|
if not value:
|
||||||
return True
|
return True
|
||||||
value_str = str(value).strip().lower()
|
value_str = str(value).strip().lower()
|
||||||
if value_str == 'no' or value_str == 'none' or value_str == 'false':
|
if value_str == 'no' or value_str == 'none' or value_str == 'false':
|
||||||
return True
|
return True
|
||||||
return False
|
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
|
||||||
|
@ -4,7 +4,7 @@ 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)
|
||||||
@ -71,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)
|
||||||
|
|
||||||
|
async def inner():
|
||||||
if args.issue_url:
|
if args.issue_url:
|
||||||
result = asyncio.run(PRAgent().handle_request(args.issue_url, [command] + args.rest))
|
result = await asyncio.create_task(PRAgent().handle_request(args.issue_url, [command] + args.rest))
|
||||||
else:
|
else:
|
||||||
result = asyncio.run(PRAgent().handle_request(args.pr_url, [command] + args.rest))
|
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()
|
||||||
|
|
||||||
|
@ -165,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:
|
||||||
@ -316,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,
|
||||||
@ -347,7 +347,7 @@ 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(
|
||||||
@ -375,12 +375,12 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
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,
|
||||||
@ -432,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)])
|
||||||
|
|
||||||
|
|
||||||
@ -516,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
|
||||||
@ -541,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
|
||||||
|
|
||||||
@ -612,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
|
||||||
|
|
||||||
|
@ -12,7 +12,13 @@ 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):
|
||||||
@ -30,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
|
||||||
@ -39,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"]
|
||||||
@ -108,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
|
||||||
|
|
||||||
@ -118,7 +130,17 @@ 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:
|
||||||
@ -129,34 +151,111 @@ class BitbucketProvider(GitProvider):
|
|||||||
if diffs != diffs_original:
|
if diffs != diffs_original:
|
||||||
try:
|
try:
|
||||||
names_original = [d.new.path for d in diffs_original]
|
names_original = [d.new.path for d in diffs_original]
|
||||||
names_filtered = [d.new.path for d in diffs]
|
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={
|
get_logger().info(f"Filtered out [ignore] files for PR", extra={
|
||||||
'original_files': names_original,
|
'original_files': names_original,
|
||||||
'filtered_files': names_filtered
|
'names_kept': names_kept,
|
||||||
|
'names_filtered': names_filtered
|
||||||
|
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
diff_split = [
|
# get the pr patches
|
||||||
"diff --git%s" % x for x in self.pr.diff().split("diff --git") if x.strip()
|
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 = []
|
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):
|
||||||
if not is_valid_file(diff.new.path):
|
file_path = _gef_filename(diff)
|
||||||
invalid_files_names.append(diff.new.path)
|
if not is_valid_file(file_path):
|
||||||
|
invalid_files_names.append(file_path)
|
||||||
continue
|
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(
|
original_file_content_str = self._get_pr_file_content(
|
||||||
diff.old.get_data("links")
|
diff.old.get_data("links")['self']['href'])
|
||||||
)
|
else:
|
||||||
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links"))
|
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':
|
||||||
@ -170,8 +269,7 @@ class BitbucketProvider(GitProvider):
|
|||||||
diff_files.append(file_patch_canonic_structure)
|
diff_files.append(file_patch_canonic_structure)
|
||||||
|
|
||||||
if invalid_files_names:
|
if invalid_files_names:
|
||||||
get_logger().info(f"Invalid file names: {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
|
||||||
@ -211,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"])
|
||||||
@ -218,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}")
|
||||||
@ -236,10 +336,13 @@ class BitbucketProvider(GitProvider):
|
|||||||
get_logger().exception(f"Failed to remove comment, error: {e}")
|
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||||
|
|
||||||
# function 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}")
|
||||||
@ -249,8 +352,8 @@ 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,
|
||||||
@ -314,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
|
||||||
|
|
||||||
@ -380,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:
|
||||||
@ -400,6 +505,13 @@ class BitbucketProvider(GitProvider):
|
|||||||
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):
|
||||||
|
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 ""
|
return ""
|
||||||
|
|
||||||
def get_commit_messages(self):
|
def get_commit_messages(self):
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
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 quote_plus, 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 ..algo.types import EDIT_TYPE, FilePatchInfo
|
from ..algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
@ -16,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
|
||||||
@ -42,22 +31,28 @@ 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:
|
||||||
|
if isinstance(e, HTTPError):
|
||||||
|
if e.response.status_code == 404: # not found
|
||||||
return ""
|
return ""
|
||||||
contents = response.text.encode('utf-8')
|
|
||||||
return contents
|
get_logger().error(f"Failed to load .pr_agent.toml file, error: {e}")
|
||||||
except Exception:
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_pr_id(self):
|
def get_pr_id(self):
|
||||||
@ -91,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,
|
||||||
@ -115,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
|
||||||
|
|
||||||
@ -131,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
|
||||||
|
|
||||||
@ -140,13 +140,51 @@ 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
|
||||||
|
|
||||||
base_sha = self.pr.toRef['latestCommit']
|
|
||||||
head_sha = self.pr.fromRef['latestCommit']
|
head_sha = self.pr.fromRef['latestCommit']
|
||||||
|
|
||||||
|
# if Bitbucket api version is >= 8.16 then use the merge-base api for 2-way diff calculation
|
||||||
|
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 = ""
|
||||||
new_file_content_str = ""
|
new_file_content_str = ""
|
||||||
@ -230,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",
|
||||||
@ -244,11 +282,18 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
requests.post(url=self._get_pr_comments_url(), json=payload, headers=self.headers).raise_for_status()
|
self.bitbucket_client.post(self._get_pr_comments_path(), data=payload)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to publish inline comment to '{file}' at line {from_line}, error: {e}")
|
get_logger().error(f"Failed to publish inline comment to '{file}' at line {from_line}, error: {e}")
|
||||||
raise 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:
|
||||||
relevant_file = suggestion['relevant_file'].strip('`').strip("'").rstrip()
|
relevant_file = suggestion['relevant_file'].strip('`').strip("'").rstrip()
|
||||||
@ -301,6 +346,9 @@ 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):
|
||||||
if hasattr(self.pr, "description"):
|
if hasattr(self.pr, "description"):
|
||||||
return self.pr.description
|
return self.pr.description
|
||||||
@ -323,14 +371,29 @@ 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(
|
||||||
f"The provided URL '{pr_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"
|
||||||
)
|
)
|
||||||
@ -350,15 +413,20 @@ 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:
|
||||||
|
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)
|
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 = {
|
payload = {
|
||||||
@ -373,7 +441,6 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
get_logger().error(f"Failed to update pull request, error: {e}")
|
get_logger().error(f"Failed to update pull request, error: {e}")
|
||||||
raise 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
|
||||||
@ -382,5 +449,8 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
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"
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -3,10 +3,11 @@ 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
|
||||||
@ -51,16 +52,28 @@ 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 split_changes_walkthrough:
|
||||||
|
description, files = process_description(description)
|
||||||
if max_tokens_description:
|
if max_tokens_description:
|
||||||
return clip_tokens(description, 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
|
return description
|
||||||
|
|
||||||
def get_user_description(self) -> str:
|
def get_user_description(self) -> str:
|
||||||
@ -74,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,
|
||||||
@ -120,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):
|
||||||
@ -166,8 +186,9 @@ class GitProvider(ABC):
|
|||||||
pass
|
pass
|
||||||
self.publish_comment(pr_comment)
|
self.publish_comment(pr_comment)
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, 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,
|
||||||
@ -238,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:
|
||||||
"""
|
"""
|
||||||
@ -308,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
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import itertools
|
||||||
import time
|
import time
|
||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -10,11 +11,11 @@ from starlette_context import context
|
|||||||
|
|
||||||
from ..algo.file_filter import filter_ignored
|
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 PRReviewHeader, 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
|
||||||
|
|
||||||
|
|
||||||
@ -25,8 +26,11 @@ class GithubProvider(GitProvider):
|
|||||||
self.installation_id = context.get("installation_id", None)
|
self.installation_id = context.get("installation_id", None)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.installation_id = None
|
self.installation_id = None
|
||||||
|
self.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
|
||||||
@ -164,18 +168,34 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
diff_files = []
|
diff_files = []
|
||||||
invalid_files_names = []
|
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)
|
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:
|
||||||
|
if avoid_load:
|
||||||
|
original_file_content_str = ""
|
||||||
else:
|
else:
|
||||||
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
|
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
|
||||||
if not patch:
|
if not patch:
|
||||||
@ -237,7 +257,7 @@ class GithubProvider(GitProvider):
|
|||||||
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
|
||||||
@ -247,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,
|
||||||
@ -425,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}
|
||||||
@ -437,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', []):
|
||||||
@ -461,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
|
||||||
|
|
||||||
@ -495,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
|
||||||
@ -505,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:
|
||||||
@ -520,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])
|
||||||
@ -549,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])
|
||||||
@ -658,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:
|
||||||
@ -676,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):
|
||||||
"""
|
"""
|
||||||
@ -731,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:
|
||||||
|
@ -4,13 +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.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
|
||||||
|
|
||||||
@ -25,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")
|
||||||
@ -32,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
|
||||||
@ -45,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
|
||||||
|
|
||||||
@ -101,13 +105,23 @@ class GitLabProvider(GitProvider):
|
|||||||
|
|
||||||
diff_files = []
|
diff_files = []
|
||||||
invalid_files_names = []
|
invalid_files_names = []
|
||||||
|
counter_valid = 0
|
||||||
for diff in diffs:
|
for diff in diffs:
|
||||||
if not is_valid_file(diff['new_path']):
|
if not is_valid_file(diff['new_path']):
|
||||||
invalid_files_names.append(diff['new_path'])
|
invalid_files_names.append(diff['new_path'])
|
||||||
continue
|
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')
|
||||||
@ -176,28 +190,33 @@ class GitLabProvider(GitProvider):
|
|||||||
self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
|
self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
|
||||||
|
|
||||||
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):
|
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 = self.mr.notes.get(comment_id)
|
||||||
comment.body = body
|
comment.body = body
|
||||||
comment.save()
|
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")
|
||||||
@ -206,11 +225,13 @@ class GitLabProvider(GitProvider):
|
|||||||
raise NotImplementedError("Gitlab provider does not support publishing inline comments yet")
|
raise NotImplementedError("Gitlab provider does not support publishing inline comments yet")
|
||||||
|
|
||||||
def get_comment_body_from_comment_id(self, comment_id: int):
|
def get_comment_body_from_comment_id(self, comment_id: int):
|
||||||
comment = self.mr.notes.get(comment_id)
|
comment = self.mr.notes.get(comment_id).body
|
||||||
return comment
|
return comment
|
||||||
|
|
||||||
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int,
|
def send_inline_comment(self, body: str, edit_type: str, found: bool, relevant_file: str,
|
||||||
source_line_no: int, target_file: str,target_line_no: int) -> None:
|
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:
|
||||||
@ -230,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.')
|
||||||
@ -257,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']
|
||||||
@ -277,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
|
||||||
|
|
||||||
@ -367,6 +444,15 @@ 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
|
||||||
|
|
||||||
@ -380,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
|
||||||
|
|
||||||
@ -423,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
|
||||||
@ -462,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
|
||||||
|
@ -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]):
|
||||||
|
@ -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])
|
||||||
|
@ -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):
|
||||||
|
@ -68,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(" ")
|
||||||
|
@ -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"
|
return "OK"
|
||||||
except KeyError:
|
|
||||||
get_logger().error("Failed to get actor type, check previous logs, this shouldn't happen.")
|
# Check if the PR should be processed
|
||||||
|
if data.get("event", "") == "pullrequest:created":
|
||||||
|
if not should_process_pr_logic(data):
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
@ -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,11 +12,14 @@ 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.git_providers.utils import apply_repo_settings
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||||
from pr_agent.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")
|
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@ -35,8 +40,11 @@ def handle_request(
|
|||||||
|
|
||||||
background_tasks.add_task(inner)
|
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()
|
||||||
@ -45,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)
|
||||||
|
|
||||||
@ -57,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"}
|
||||||
|
@ -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:
|
||||||
@ -83,7 +83,11 @@ async def run_action():
|
|||||||
# Handle pull request event
|
# Handle pull request event
|
||||||
if GITHUB_EVENT_NAME == "pull_request":
|
if GITHUB_EVENT_NAME == "pull_request":
|
||||||
action = event_payload.get("action")
|
action = event_payload.get("action")
|
||||||
if action in ["opened", "reopened", "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
|
||||||
|
@ -128,8 +128,6 @@ async def handle_new_pr_opened(body: Dict[str, Any],
|
|||||||
log_context: Dict[str, Any],
|
log_context: Dict[str, Any],
|
||||||
agent: PRAgent):
|
agent: PRAgent):
|
||||||
title = body.get("pull_request", {}).get("title", "")
|
title = body.get("pull_request", {}).get("title", "")
|
||||||
get_settings().config.is_auto_command = True
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@ -138,13 +136,6 @@ async def handle_new_pr_opened(body: Dict[str, Any],
|
|||||||
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] ...")
|
# logic to ignore PRs with specific titles (e.g. "[Auto] ...")
|
||||||
apply_repo_settings(api_url)
|
apply_repo_settings(api_url)
|
||||||
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 {}
|
|
||||||
|
|
||||||
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:
|
||||||
@ -246,6 +237,60 @@ def get_log_context(body, event, action, build_number):
|
|||||||
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.
|
||||||
@ -260,10 +305,11 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
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:
|
return {}
|
||||||
get_logger().info(f"Ignoring PR from '{sender=}' because it is a bot")
|
if action != 'created' and 'check_run' not in body:
|
||||||
|
if not should_process_pr_logic(sender_type, sender, body):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if 'check_run' in body: # handle failed checks
|
if 'check_run' in body: # handle failed checks
|
||||||
@ -281,7 +327,6 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
pass # handle_checkbox_clicked
|
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) # added inside handle_push_trigger_for_new_commits
|
|
||||||
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", ""):
|
||||||
@ -325,12 +370,14 @@ 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:
|
||||||
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]
|
||||||
@ -349,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)
|
||||||
|
@ -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,6 +16,15 @@ 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.
|
||||||
@ -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,43 +187,52 @@ 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']
|
|
||||||
async with session.get(latest_comment, headers=headers) as comment_response:
|
|
||||||
if comment_response.status == 200:
|
|
||||||
comment = await comment_response.json()
|
|
||||||
if 'id' in comment:
|
|
||||||
if comment['id'] in handled_ids:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
handled_ids.add(comment['id'])
|
|
||||||
if 'user' in comment and 'login' in comment['user']:
|
|
||||||
if comment['user']['login'] == user_id:
|
|
||||||
continue
|
|
||||||
comment_body = comment['body'] if 'body' in comment else ''
|
|
||||||
commenter_github_user = comment['user']['login'] \
|
|
||||||
if 'user' in comment else ''
|
|
||||||
get_logger().info(f"Commenter: {commenter_github_user}\nComment: {comment_body}")
|
|
||||||
user_tag = "@" + user_id
|
|
||||||
if user_tag not in comment_body:
|
|
||||||
continue
|
|
||||||
rest_of_comment = comment_body.split(user_tag)[1].strip()
|
rest_of_comment = comment_body.split(user_tag)[1].strip()
|
||||||
comment_id = comment['id']
|
comment_id = comment['id']
|
||||||
git_provider.set_pr(pr_url)
|
|
||||||
success = await agent.handle_request(pr_url, rest_of_comment,
|
# Add to the task queue
|
||||||
notify=lambda: git_provider.add_eyes_reaction(comment_id)) # noqa E501
|
get_logger().info(
|
||||||
if not success:
|
f"Adding comment processing to task queue for PR, {pr_url}, comment_body: {comment_body}")
|
||||||
git_provider.set_pr(pr_url)
|
task_queue.append((process_comment_sync, (pr_url, rest_of_comment, comment_id)))
|
||||||
|
get_logger().info(f"Queued comment processing for PR: {pr_url}")
|
||||||
|
else:
|
||||||
|
get_logger().debug(f"Skipping comment processing for PR")
|
||||||
|
|
||||||
|
max_allowed_parallel_tasks = 10
|
||||||
|
if task_queue:
|
||||||
|
processes = []
|
||||||
|
for i, (func, args) in enumerate(task_queue): # Create parallel tasks
|
||||||
|
p = multiprocessing.Process(target=func, args=args)
|
||||||
|
processes.append(p)
|
||||||
|
p.start()
|
||||||
|
if i > max_allowed_parallel_tasks:
|
||||||
|
get_logger().error(
|
||||||
|
f"Dropping {len(task_queue) - max_allowed_parallel_tasks} tasks from polling session")
|
||||||
|
break
|
||||||
|
task_queue.clear()
|
||||||
|
|
||||||
|
# Dont wait for all processes to complete. Move on to the next iteration
|
||||||
|
# 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__':
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ async def handle_request(api_url: str, body: str, log_context: dict, sender_id:
|
|||||||
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"] = api_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):
|
||||||
await PRAgent().handle_request(api_url, body)
|
await PRAgent().handle_request(api_url, body)
|
||||||
@ -60,6 +62,7 @@ async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url:
|
|||||||
log_context: dict):
|
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(" ")
|
||||||
@ -74,6 +77,57 @@ 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):
|
||||||
@ -86,6 +140,10 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
|||||||
if request.headers.get("X-Gitlab-Token") and secret_provider:
|
if request.headers.get("X-Gitlab-Token") and secret_provider:
|
||||||
request_token = request.headers.get("X-Gitlab-Token")
|
request_token = request.headers.get("X-Gitlab-Token")
|
||||||
secret = secret_provider.get_secret(request_token)
|
secret = secret_provider.get_secret(request_token)
|
||||||
|
if not secret:
|
||||||
|
get_logger().warning(f"Empty secret retrieved, request_token: {request_token}")
|
||||||
|
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
content=jsonable_encoder({"message": "unauthorized"}))
|
||||||
try:
|
try:
|
||||||
secret_dict = json.loads(secret)
|
secret_dict = json.loads(secret)
|
||||||
gitlab_token = secret_dict["gitlab_token"]
|
gitlab_token = secret_dict["gitlab_token"]
|
||||||
@ -112,21 +170,30 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
|||||||
sender = data.get("user", {}).get("username", "unknown")
|
sender = data.get("user", {}).get("username", "unknown")
|
||||||
sender_id = data.get("user", {}).get("id", "unknown")
|
sender_id = data.get("user", {}).get("id", "unknown")
|
||||||
|
|
||||||
# logic to ignore bot users (unlike Github, no direct flag for bot users in gitlab)
|
# ignore bot users
|
||||||
sender_name = data.get("user", {}).get("name", "unknown").lower()
|
if is_bot_user(data):
|
||||||
if 'codium' in sender_name or 'bot_' in sender_name or 'bot-' in sender_name or '_bot' in sender_name or '-bot' in sender_name:
|
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
||||||
get_logger().info(f"Skipping bot user: {sender_name}")
|
if data.get('event_type') != 'note' and data.get('object_attributes', {}): # not a comment
|
||||||
|
# ignore MRs based on title, labels, source and target branches
|
||||||
|
if not should_process_pr_logic(data, data['object_attributes'].get('title')):
|
||||||
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"}))
|
||||||
|
|
||||||
log_context["sender"] = sender
|
log_context["sender"] = sender
|
||||||
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
||||||
|
title = data['object_attributes'].get('title')
|
||||||
url = data['object_attributes'].get('url')
|
url = data['object_attributes'].get('url')
|
||||||
|
draft = data['object_attributes'].get('draft')
|
||||||
get_logger().info(f"New merge request: {url}")
|
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)
|
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
|
elif data.get('object_kind') == 'note' and data.get('event_type') == 'note': # comment on MR
|
||||||
if 'merge_request' in data:
|
if 'merge_request' in data:
|
||||||
mr = data['merge_request']
|
mr = data['merge_request']
|
||||||
url = mr.get('url')
|
url = mr.get('url')
|
||||||
|
|
||||||
get_logger().info(f"A comment has been added to a merge request: {url}")
|
get_logger().info(f"A comment has been added to a merge request: {url}")
|
||||||
body = data.get('object_attributes', {}).get('note')
|
body = data.get('object_attributes', {}).get('note')
|
||||||
if data.get('object_attributes', {}).get('type') == 'DiffNote' and '/ask' in body: # /ask_line
|
if data.get('object_attributes', {}).get('type') == 'DiffNote' and '/ask' in body: # /ask_line
|
||||||
|
@ -43,9 +43,6 @@ 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.
|
||||||
@ -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
|
||||||
|
@ -1,27 +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
|
||||||
|
# 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=""
|
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"
|
large_patch_policy = "clip" # "clip", "skip"
|
||||||
is_auto_command=false
|
# 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
|
||||||
@ -86,7 +107,7 @@ enable_help_text=false
|
|||||||
|
|
||||||
|
|
||||||
[pr_code_suggestions] # /improve #
|
[pr_code_suggestions] # /improve #
|
||||||
max_context_tokens=10000
|
max_context_tokens=14000
|
||||||
num_code_suggestions=4
|
num_code_suggestions=4
|
||||||
commitable_code_suggestions = false
|
commitable_code_suggestions = false
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
@ -173,11 +194,13 @@ 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"
|
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
|
||||||
|
# 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
|
||||||
@ -201,20 +224,11 @@ 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 to the gitlab service
|
|
||||||
url = "https://gitlab.com"
|
url = "https://gitlab.com"
|
||||||
# Polling (either project id or namespace/project_name) syntax can be used
|
|
||||||
projects_to_monitor = ['org_name/repo_name']
|
|
||||||
# Polling trigger
|
|
||||||
magic_word = "AutoReview"
|
|
||||||
# Polling interval
|
|
||||||
polling_interval_seconds = 30
|
|
||||||
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",
|
||||||
]
|
]
|
||||||
@ -226,10 +240,11 @@ push_commands = [
|
|||||||
|
|
||||||
[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 --pr_code_suggestions.suggestions_score_threshold=7",
|
"/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
|
||||||
@ -250,10 +265,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
|
||||||
@ -276,3 +300,7 @@ number_of_results = 5
|
|||||||
|
|
||||||
[lancedb]
|
[lancedb]
|
||||||
uri = "./lancedb"
|
uri = "./lancedb"
|
||||||
|
[best_practices]
|
||||||
|
content = ""
|
||||||
|
max_lines_allowed = 800
|
||||||
|
enable_global_best_practices = false
|
@ -63,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", ]
|
||||||
|
@ -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: |-
|
||||||
|
@ -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, a language model that specializes in suggesting improvements to a Pull Request (PR) code.
|
||||||
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR diff.
|
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR code diff (lines starting with '+').
|
||||||
|
|
||||||
|
|
||||||
The format we will use to present the PR code diff:
|
The format we will use to present the PR code diff:
|
||||||
======
|
======
|
||||||
## 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,23 +31,25 @@ __old hunk__
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
## file: 'src/file2.py'
|
## File: 'src/file2.py'
|
||||||
...
|
...
|
||||||
======
|
======
|
||||||
- In this format, we separated each hunk of diff 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.
|
|
||||||
- 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 ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. \
|
|
||||||
Suggestions should always focus on ways to improve the new code lines introduced in the PR, meaning lines in the '__new hunk__' sections that begin with a '+' symbol (after the line numbers). The '__old hunk__' sections code is for context and reference only.
|
|
||||||
|
|
||||||
|
- In this format, we separate each hunk of diff 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. If no new code was added in a specific hunk, '__new hunk__' section will not be presented. If no code was removed, '__old hunk__' section will not be presented.
|
||||||
|
- We also added line numbers for the '__new hunk__' code, to help you refer to the code lines in your suggestions. These line numbers are not part of the actual code, and should only used for reference.
|
||||||
|
- Code lines are prefixed with symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. \
|
||||||
|
{%- if is_ai_metadata %}
|
||||||
|
- If available, an AI-generated summary will appear and provide a high-level overview of the file changes. Note that this summary may not be fully accurate or complete.
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
Specific instructions for generating code suggestions:
|
Specific instructions 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 }} code suggestions.
|
||||||
- The suggestions should focus on improving the new code introduced the PR, meaning lines from '__new hunk__' sections, starting with '+' (after the line numbers).
|
- The suggestions should be diverse and insightful. They should focus on improving only the new code introduced in the PR, meaning lines from '__new hunk__' sections, starting with '+' (after the line numbers).
|
||||||
- Prioritize suggestions that address possible issues, major problems, and bugs in the PR code.
|
- Prioritize suggestions that address possible issues, major problems, and bugs in the PR code. Don't repeat changes already present in the PR. If there are no relevant suggestions for the PR, return an empty list.
|
||||||
- Don't suggest to add docstring, type hints, or comments, or to remove unused imports.
|
- Don't suggest to add docstring, type hints, or comments, or to remove unused imports.
|
||||||
- Suggestions should not repeat code already present in the '__new hunk__' sections.
|
- Suggestions should not repeat code already present in the '__new hunk__' sections.
|
||||||
- Provide the exact line numbers range (inclusive) for each suggestion. Use the line numbers from the '__new hunk__' sections.
|
- Provide the exact line numbers range (inclusive) for each suggestion. Use the line numbers from the '__new hunk__' sections.
|
||||||
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
- Every time you cite variables or names from the code, use backticks ('`'). For example: 'ensure that `variable_name` is ...'
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|
||||||
@ -57,10 +66,10 @@ 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="The full file path of the relevant file")
|
||||||
language: str = Field(description="the code language of the relevant file")
|
language: str = Field(description="The programming language of 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 for meaningfully improving the new code introduced in the PR")
|
||||||
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, demonstrating the relevant code lines from a '__new hunk__' section. It must be without line numbers. Quote only full code lines, not partial ones. Use abbreviations ("...") of full lines if needed")
|
||||||
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 new code snippet, that can be used to replace the relevant 'existing_code' lines in '__new hunk__' code after applying 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 short summary of the suggestion action, in a single sentence. 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 'existing code' snippet above")
|
||||||
@ -97,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}}'
|
||||||
|
|
||||||
@ -114,23 +123,30 @@ Response (should be a valid YAML, and nothing else):
|
|||||||
|
|
||||||
|
|
||||||
[pr_code_suggestions_prompt_claude]
|
[pr_code_suggestions_prompt_claude]
|
||||||
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, a language model that specializes in suggesting improvements to a Pull Request (PR) code.
|
||||||
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR diff.
|
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR code diff (lines starting with '+').
|
||||||
|
|
||||||
|
|
||||||
The format we will use to present the PR code diff:
|
The format we will use to present the PR code diff:
|
||||||
======
|
======
|
||||||
## 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__
|
||||||
@ -139,22 +155,24 @@ __old hunk__
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
## file: 'src/file2.py'
|
## File: 'src/file2.py'
|
||||||
...
|
...
|
||||||
======
|
======
|
||||||
- In this format, we separated each hunk of diff 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.
|
|
||||||
- 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 ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. \
|
|
||||||
Suggestions should always focus on ways to improve the new code lines introduced in the PR, meaning lines in the '__new hunk__' sections that begin with a '+' symbol (after the line numbers). The '__old hunk__' sections code is for context and reference only.
|
|
||||||
|
|
||||||
|
- In this format, we separate each hunk of diff 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. If no new code was added in a specific hunk, '__new hunk__' section will not be presented. If no code was removed, '__old hunk__' section will not be presented.
|
||||||
|
- We also added line numbers for the '__new hunk__' code, to help you refer to the code lines in your suggestions. These line numbers are not part of the actual code, and should only used for reference.
|
||||||
|
- Code lines are prefixed with symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. \
|
||||||
|
{%- if is_ai_metadata %}
|
||||||
|
- If available, an AI-generated summary will appear and provide a high-level overview of the file changes. Note that this summary may not be fully accurate or complete.
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
Specific instructions for generating code suggestions:
|
Specific instructions 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 }} code suggestions.
|
||||||
- The suggestions should focus on improving the new code introduced the PR, meaning lines from '__new hunk__' sections, starting with '+' (after the line numbers).
|
- The suggestions should be diverse and insightful. They should focus on improving only the new code introduced in the PR, meaning lines from '__new hunk__' sections, starting with '+' (after the line numbers).
|
||||||
- Prioritize suggestions that address possible issues, major problems, and bugs in the PR code.
|
- Prioritize suggestions that address possible issues, major problems, and bugs in the PR code. Don't repeat changes already present in the PR. If there are no relevant suggestions for the PR, return an empty list.
|
||||||
- Don't suggest to add docstring, type hints, or comments, or to remove unused imports.
|
- Don't suggest to add docstring, type hints, or comments, or to remove unused imports.
|
||||||
- Provide the exact line numbers range (inclusive) for each suggestion. Use the line numbers from the '__new hunk__' sections.
|
- Provide the exact line numbers range (inclusive) for each suggestion. Use the line numbers from the '__new hunk__' sections.
|
||||||
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
- Every time you cite variables or names from the code, use backticks ('`'). For example: 'ensure that `variable_name` is ...'
|
||||||
- Take into account that you are recieving as an input only a PR code diff. The entire codebase is not available for you as context. Hence, avoid suggestions that might conflict with unseen parts of the codebase, like imports, global variables, etc.
|
- Take into account that you are recieving as an input only a PR code diff. The entire codebase is not available for you as context. Hence, avoid suggestions that might conflict with unseen parts of the codebase, like imports, global variables, etc.
|
||||||
|
|
||||||
|
|
||||||
@ -171,15 +189,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="The full file path of the relevant file")
|
||||||
language: str = Field(description="the code language of the relevant file")
|
language: str = Field(description="the programming language of the relevant file")
|
||||||
suggestion_content: str = Field(description="an actionable suggestion for meaningfully improving the new code introduced in the PR. Don't present here actual code snippets, just the suggestion. Be short and concise")
|
suggestion_content: str = Field(description="an actionable suggestion for meaningfully improving 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, demonstrating the relevant code lines from a '__new hunk__' section. It must be without line numbers. Quote only full code lines, not partial ones. Use abbreviations ("...") of full lines if needed")
|
||||||
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 new code snippet, that can be used to replace the relevant 'existing_code' lines in '__new hunk__' code after applying 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 short summary of the suggestion action, in a single sentence. 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 '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 'existing code' snippet above")
|
||||||
label: str = Field(description="a single label for the suggestion, to help 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 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")
|
||||||
|
|
||||||
|
|
||||||
class PRCodeSuggestions(BaseModel):
|
class PRCodeSuggestions(BaseModel):
|
||||||
code_suggestions: List[CodeSuggestion]
|
code_suggestions: List[CodeSuggestion]
|
||||||
|
@ -6,26 +6,26 @@ Your goal is to inspect, review and score the suggestsions.
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
Specific instructions:
|
Specific instructions:
|
||||||
- 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.
|
- Carefully review both the suggestion content, and the related PR code diff. Mistakes in the suggestions can occur. Make sure the suggestions are logical and correct, and properly derived from the PR code diff.
|
||||||
- In addition to the exact code lines mentioned in each suggestion, review the code around them, to ensure that the suggestions are contextually accurate.
|
- In addition to the exact code lines mentioned in each suggestion, review the code around them, to ensure that the suggestions are contextually accurate.
|
||||||
- Also check that the 'existing_code' and 'improved_code' fields correctly reflect the suggested changes.
|
- Check that the 'existing_code' field is valid. The 'existing_code' content should match, or be derived, from code lines from a 'new hunk' section in the PR code diff.
|
||||||
- Make sure the suggestions focus on new code introduced in the PR, and not on existing code that was not changed.
|
- Check that the 'improved_code' section correctly reflects the suggestion content.
|
||||||
- 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.
|
- 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.
|
- Order the feedback the same way the suggestions are ordered in the input.
|
||||||
|
|
||||||
|
|
||||||
The format that is used to present the PR code diff is as follows:
|
The format that is used to present the PR code diff is as follows:
|
||||||
======
|
======
|
||||||
## file: 'src/file1.py'
|
## File: 'src/file1.py'
|
||||||
|
|
||||||
@@ ... @@ def func1():
|
@@ ... @@ def func1():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
12 code line1 that remained unchanged in the PR
|
12 code line1 that remained unchanged in the PR
|
||||||
13 +new hunk code line2 added in the PR
|
13 +new code line2 added in the PR
|
||||||
14 code line3 that remained unchanged in the PR
|
14 code line3 that remained unchanged in the PR
|
||||||
__old hunk__
|
__old hunk__
|
||||||
code line1 that remained unchanged in the PR
|
code line1 that remained unchanged in the PR
|
||||||
-old hunk code line2 that was removed in the PR
|
-old code line2 that was removed in the PR
|
||||||
code line3 that remained unchanged in the PR
|
code line3 that remained unchanged in the PR
|
||||||
|
|
||||||
@@ ... @@ def func2():
|
@@ ... @@ def func2():
|
||||||
@ -35,12 +35,13 @@ __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 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.
|
||||||
|
- If no new code was added in a specific hunk, '__new hunk__' section will not be presented. If no code was removed, '__old hunk__' section will not be presented.
|
||||||
|
- We 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 symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code.
|
- 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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@ -48,7 +49,7 @@ The output must be a YAML object equivalent to type $PRCodeSuggestionsFeedback,
|
|||||||
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="The actual output - the score of the suggestion, from 0 to 10. Give 0 if the suggestion is wrong. Otherwise, give a score from 1 to 10 (inclusive), where 1 is the lowest and 10 is the highest.")
|
||||||
why: str = Field(description="Short and concise explanation of why the suggestion received the score (one to two sentences).")
|
why: str = Field(description="Short and concise explanation of why the suggestion received the score (one to two sentences).")
|
||||||
|
|
||||||
class PRCodeSuggestionsFeedback(BaseModel):
|
class PRCodeSuggestionsFeedback(BaseModel):
|
||||||
|
@ -38,8 +38,8 @@ 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', ...")
|
||||||
@ -48,7 +48,7 @@ class FileDescription(BaseModel):
|
|||||||
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")
|
||||||
|
@ -12,7 +12,7 @@ Additional guidelines:
|
|||||||
|
|
||||||
Example Hunk Structure:
|
Example Hunk Structure:
|
||||||
======
|
======
|
||||||
## file: 'src/file1.py'
|
## File: 'src/file1.py'
|
||||||
|
|
||||||
@@ -12,5 +12,5 @@ def func1():
|
@@ -12,5 +12,5 @@ def func1():
|
||||||
code line 1 that remained unchanged in the PR
|
code line 1 that remained unchanged in the PR
|
||||||
|
@ -5,41 +5,65 @@ Your task is to provide constructive and concise feedback for the PR, and also p
|
|||||||
{%- else %}
|
{%- else %}
|
||||||
Your task is to provide constructive and concise feedback for the PR.
|
Your task is to provide constructive and concise feedback for the PR.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
The review should focus on new code added in the PR diff (lines starting with '+')
|
The review should focus on new code added in the PR code diff (lines starting with '+')
|
||||||
|
|
||||||
Example PR Diff:
|
|
||||||
|
The format we will use to present the PR code diff:
|
||||||
======
|
======
|
||||||
## file: 'src/file1.py'
|
## File: 'src/file1.py'
|
||||||
|
{%- if is_ai_metadata %}
|
||||||
|
### AI-generated changes summary:
|
||||||
|
* ...
|
||||||
|
* ...
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
@@ -12,5 +12,5 @@ def func1():
|
|
||||||
code line 1 that remained unchanged in the PR
|
@@ ... @@ def func1():
|
||||||
code line 2 that remained unchanged in the PR
|
__new hunk__
|
||||||
-code line that was removed in the PR
|
11 unchanged code line0 in the PR
|
||||||
+code line added in the PR
|
12 unchanged code line1 in the PR
|
||||||
code line 3 that remained unchanged 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():
|
@@ ... @@ def func2():
|
||||||
|
__new hunk__
|
||||||
|
...
|
||||||
|
__old hunk__
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
## file: 'src/file2.py'
|
## File: 'src/file2.py'
|
||||||
...
|
...
|
||||||
======
|
======
|
||||||
|
|
||||||
|
- In this format, we separated each hunk of diff 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. If no new code was added in a specific hunk, '__new hunk__' section will not be presented. If no code was removed, '__old hunk__' section will not be presented.
|
||||||
|
- We also added line numbers for the '__new hunk__' code, to help you refer to the code lines in your suggestions. These line numbers are not part of the actual code, and should only used for reference.
|
||||||
|
- Code lines are prefixed with symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. \
|
||||||
|
The review should address new code added in the PR code diff (lines starting with '+')
|
||||||
|
{%- if is_ai_metadata %}
|
||||||
|
- If available, an AI-generated summary will appear and provide a high-level overview of the file changes. Note that this summary may not be fully accurate or complete.
|
||||||
|
{%- endif %}
|
||||||
|
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
||||||
|
|
||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
|
||||||
|
|
||||||
Code suggestions guidelines:
|
Code suggestions guidelines:
|
||||||
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
|
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
|
||||||
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements like performance, vulnerability, modularity, and best practices.
|
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
|
||||||
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
|
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
|
||||||
- Don't suggest to add docstring, type hints, or comments.
|
- Don't suggest to add docstring, type hints, or comments.
|
||||||
- Suggestions should focus on the new code added in the PR diff (lines starting with '+')
|
- Suggestions should address the new code added in the PR diff (lines starting with '+')
|
||||||
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
|
|
||||||
Extra instructions from the user:
|
Extra instructions from the user:
|
||||||
======
|
======
|
||||||
{{ extra_instructions }}
|
{{ extra_instructions }}
|
||||||
@ -55,6 +79,13 @@ class SubPR(BaseModel):
|
|||||||
title: str = Field(description="Short and concise title for an independent and meaningful sub-PR, composed only from the relevant files")
|
title: str = Field(description="Short and concise title for an independent and meaningful sub-PR, composed only from the relevant files")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
class KeyIssuesComponentLink(BaseModel):
|
||||||
|
relevant_file: str = Field(description="The full file path of the relevant file")
|
||||||
|
issue_header: str = Field(description="one or two word title for the the issue. For example: 'Possible Bug', 'Performance Issue', 'Code Smell', etc.")
|
||||||
|
issue_content: str = Field(description="a short and concise description of the issue that needs to be reviewed")
|
||||||
|
start_line: int = Field(description="the start line that corresponds to this issue in the relevant file")
|
||||||
|
end_line: int = Field(description="the end line that corresponds to this issue in the relevant file")
|
||||||
|
|
||||||
class Review(BaseModel):
|
class Review(BaseModel):
|
||||||
{%- if require_estimate_effort_to_review %}
|
{%- if require_estimate_effort_to_review %}
|
||||||
estimated_effort_to_review_[1-5]: int = Field(description="Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review. Take into account the size, complexity, quality, and the needed changes of the PR code diff.")
|
estimated_effort_to_review_[1-5]: int = Field(description="Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review. Take into account the size, complexity, quality, and the needed changes of the PR code diff.")
|
||||||
@ -68,9 +99,9 @@ class Review(BaseModel):
|
|||||||
{%- if question_str %}
|
{%- if question_str %}
|
||||||
insights_from_user_answers: str = Field(description="shortly summarize the insights you gained from the user's answers to the questions")
|
insights_from_user_answers: str = Field(description="shortly summarize the insights you gained from the user's answers to the questions")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
key_issues_to_review: str = Field(description="Does this PR code introduce issues, bugs, or major performance concerns, which the PR reviewer should further investigate ? If there are no apparent issues, respond with 'None'. If there are any issues, describe them briefly. Use bullet points if more than one issue. Be specific, and provide examples if possible. Start each bullet point with a short specific header, such as: "- Possible Bug: ...", etc.")
|
key_issues_to_review: List[KeyIssuesComponentLink] = Field("A list of bugs, issue or major performance concerns introduced in this PR, which the PR reviewer should further investigate")
|
||||||
{%- if require_security_review %}
|
{%- if require_security_review %}
|
||||||
security_concerns: str = Field(description="does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? Answer 'No' if there are no possible issues. If there are security concerns or issues, start your answer with a short header, such as: 'Sensitive information exposure: ...', 'SQL injection: ...' etc. Explain your answer. Be specific and give examples if possible")
|
security_concerns: str = Field(description="Does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? Answer 'No' (without explaining why) if there are no possible issues. If there are security concerns or issues, start your answer with a short header, such as: 'Sensitive information exposure: ...', 'SQL injection: ...' etc. Explain your answer. Be specific and give examples if possible")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if require_can_be_split_review %}
|
{%- if require_can_be_split_review %}
|
||||||
can_be_split: List[SubPR] = Field(min_items=0, max_items=3, description="Can this PR, which contains {{ num_pr_files }} changed files in total, be divided into smaller sub-PRs with distinct tasks that can be reviewed and merged independently, regardless of the order ? Make sure that the sub-PRs are indeed independent, with no code dependencies between them, and that each sub-PR represent a meaningful independent task. Output an empty list if the PR code does not need to be split.")
|
can_be_split: List[SubPR] = Field(min_items=0, max_items=3, description="Can this PR, which contains {{ num_pr_files }} changed files in total, be divided into smaller sub-PRs with distinct tasks that can be reviewed and merged independently, regardless of the order ? Make sure that the sub-PRs are indeed independent, with no code dependencies between them, and that each sub-PR represent a meaningful independent task. Output an empty list if the PR code does not need to be split.")
|
||||||
@ -78,8 +109,8 @@ class Review(BaseModel):
|
|||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
|
||||||
class CodeSuggestion(BaseModel):
|
class CodeSuggestion(BaseModel):
|
||||||
relevant_file: str = Field(description="the relevant file full path")
|
relevant_file: str = Field(description="The full file path of the relevant file")
|
||||||
language: str = Field(description="the language of the relevant file")
|
language: str = Field(description="The programming language of the relevant file")
|
||||||
suggestion: str = Field(description="a concrete suggestion for meaningfully improving the new PR code. Also describe how, specifically, the suggestion can be applied to new PR code. Add tags with importance measure that matches each suggestion ('important' or 'medium'). Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.")
|
suggestion: str = Field(description="a concrete suggestion for meaningfully improving the new PR code. Also describe how, specifically, the suggestion can be applied to new PR code. Add tags with importance measure that matches each suggestion ('important' or 'medium'). Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.")
|
||||||
relevant_line: str = Field(description="a single code line taken from the relevant file, to which the suggestion applies. The code line should start with a '+'. Make sure to output the line exactly as it appears in the relevant file")
|
relevant_line: str = Field(description="a single code line taken from the relevant file, to which the suggestion applies. The code line should start with a '+'. Make sure to output the line exactly as it appears in the relevant file")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@ -90,6 +121,7 @@ class PRReview(BaseModel):
|
|||||||
code_feedback: List[CodeSuggestion]
|
code_feedback: List[CodeSuggestion]
|
||||||
{%- else %}
|
{%- else %}
|
||||||
|
|
||||||
|
|
||||||
class PRReview(BaseModel):
|
class PRReview(BaseModel):
|
||||||
review: Review
|
review: Review
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@ -108,8 +140,16 @@ review:
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
relevant_tests: |
|
relevant_tests: |
|
||||||
No
|
No
|
||||||
key_issues_to_review: |
|
key_issues_to_review:
|
||||||
|
- relevant_file: |
|
||||||
|
directory/xxx.py
|
||||||
|
issue_header: |
|
||||||
|
Possible Bug
|
||||||
|
issue_content: |
|
||||||
...
|
...
|
||||||
|
start_line: 12
|
||||||
|
end_line: 14
|
||||||
|
- ...
|
||||||
security_concerns: |
|
security_concerns: |
|
||||||
No
|
No
|
||||||
{%- if require_can_be_split_review %}
|
{%- if require_can_be_split_review %}
|
||||||
@ -120,8 +160,9 @@ review:
|
|||||||
title: ...
|
title: ...
|
||||||
- ...
|
- ...
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
code_feedback
|
code_feedback:
|
||||||
- relevant_file: |
|
- relevant_file: |
|
||||||
directory/xxx.py
|
directory/xxx.py
|
||||||
language: |
|
language: |
|
||||||
@ -136,7 +177,7 @@ code_feedback
|
|||||||
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
|
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user="""PR Info:
|
user="""--PR Info--
|
||||||
|
|
||||||
Title: '{{title}}'
|
Title: '{{title}}'
|
||||||
|
|
||||||
@ -144,7 +185,7 @@ Branch: '{{branch}}'
|
|||||||
|
|
||||||
{%- if description %}
|
{%- if description %}
|
||||||
|
|
||||||
Description:
|
PR Description:
|
||||||
======
|
======
|
||||||
{{ description|trim }}
|
{{ description|trim }}
|
||||||
======
|
======
|
||||||
@ -165,7 +206,7 @@ User answers:
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
The PR Diff:
|
The PR code diff:
|
||||||
======
|
======
|
||||||
{{ diff|trim }}
|
{{ diff|trim }}
|
||||||
======
|
======
|
||||||
|
@ -89,8 +89,8 @@ class PRAddDocs:
|
|||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
system=system_prompt, user=user_prompt)
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -7,11 +7,13 @@ from jinja2 import Environment, StrictUndefined
|
|||||||
|
|
||||||
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.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models, \
|
||||||
|
add_ai_metadata_to_diff_files
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import load_yaml, replace_code_tags, ModelType, show_relevant_configurations
|
from pr_agent.algo.utils import load_yaml, replace_code_tags, ModelType, show_relevant_configurations
|
||||||
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, get_git_provider_with_context, GithubProvider, GitLabProvider
|
from pr_agent.git_providers import get_git_provider, get_git_provider_with_context, GithubProvider, GitLabProvider, \
|
||||||
|
AzureDevopsProvider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
from pr_agent.servers.help import HelpMessage
|
from pr_agent.servers.help import HelpMessage
|
||||||
@ -34,6 +36,7 @@ class PRCodeSuggestions:
|
|||||||
MAX_CONTEXT_TOKENS_IMPROVE = get_settings().pr_code_suggestions.max_context_tokens
|
MAX_CONTEXT_TOKENS_IMPROVE = get_settings().pr_code_suggestions.max_context_tokens
|
||||||
if get_settings().config.max_model_tokens > MAX_CONTEXT_TOKENS_IMPROVE:
|
if get_settings().config.max_model_tokens > MAX_CONTEXT_TOKENS_IMPROVE:
|
||||||
get_logger().info(f"Setting max_model_tokens to {MAX_CONTEXT_TOKENS_IMPROVE} for PR improve")
|
get_logger().info(f"Setting max_model_tokens to {MAX_CONTEXT_TOKENS_IMPROVE} for PR improve")
|
||||||
|
get_settings().config.max_model_tokens_original = get_settings().config.max_model_tokens
|
||||||
get_settings().config.max_model_tokens = MAX_CONTEXT_TOKENS_IMPROVE
|
get_settings().config.max_model_tokens = MAX_CONTEXT_TOKENS_IMPROVE
|
||||||
|
|
||||||
# extended mode
|
# extended mode
|
||||||
@ -50,16 +53,29 @@ class PRCodeSuggestions:
|
|||||||
self.ai_handler.main_pr_language = self.main_language
|
self.ai_handler.main_pr_language = self.main_language
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
|
self.pr_url = pr_url
|
||||||
self.cli_mode = cli_mode
|
self.cli_mode = cli_mode
|
||||||
|
self.pr_description, self.pr_description_files = (
|
||||||
|
self.git_provider.get_pr_description(split_changes_walkthrough=True))
|
||||||
|
if (self.pr_description_files and get_settings().get("config.is_auto_command", False) and
|
||||||
|
get_settings().get("config.enable_ai_metadata", False)):
|
||||||
|
add_ai_metadata_to_diff_files(self.git_provider, self.pr_description_files)
|
||||||
|
get_logger().debug(f"AI metadata added to the this command")
|
||||||
|
else:
|
||||||
|
get_settings().set("config.enable_ai_metadata", False)
|
||||||
|
get_logger().debug(f"AI metadata is disabled for this command")
|
||||||
|
|
||||||
self.vars = {
|
self.vars = {
|
||||||
"title": self.git_provider.pr.title,
|
"title": self.git_provider.pr.title,
|
||||||
"branch": self.git_provider.get_pr_branch(),
|
"branch": self.git_provider.get_pr_branch(),
|
||||||
"description": self.git_provider.get_pr_description(),
|
"description": self.pr_description,
|
||||||
"language": self.main_language,
|
"language": self.main_language,
|
||||||
"diff": "", # empty diff for initial calculation
|
"diff": "", # empty diff for initial calculation
|
||||||
"num_code_suggestions": num_code_suggestions,
|
"num_code_suggestions": num_code_suggestions,
|
||||||
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
|
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
|
"relevant_best_practices": "",
|
||||||
|
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
||||||
}
|
}
|
||||||
if 'claude' in get_settings().config.model:
|
if 'claude' in get_settings().config.model:
|
||||||
# prompt for Claude, with minor adjustments
|
# prompt for Claude, with minor adjustments
|
||||||
@ -78,6 +94,10 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
try:
|
try:
|
||||||
|
if not self.git_provider.get_files():
|
||||||
|
get_logger().info(f"PR has no files: {self.pr_url}, skipping code suggestions")
|
||||||
|
return None
|
||||||
|
|
||||||
get_logger().info('Generating code suggestions for PR...')
|
get_logger().info('Generating code suggestions for PR...')
|
||||||
relevant_configs = {'pr_code_suggestions': dict(get_settings().pr_code_suggestions),
|
relevant_configs = {'pr_code_suggestions': dict(get_settings().pr_code_suggestions),
|
||||||
'config': dict(get_settings().config)}
|
'config': dict(get_settings().config)}
|
||||||
@ -96,9 +116,10 @@ class PRCodeSuggestions:
|
|||||||
if not data:
|
if not data:
|
||||||
data = {"code_suggestions": []}
|
data = {"code_suggestions": []}
|
||||||
|
|
||||||
if data is None or 'code_suggestions' not in data or not data['code_suggestions']:
|
if (data is None or 'code_suggestions' not in data or not data['code_suggestions']
|
||||||
get_logger().error('No code suggestions found for PR.')
|
and get_settings().config.publish_output):
|
||||||
pr_body = "## PR Code Suggestions ✨\n\nNo code suggestions found for PR."
|
get_logger().warning('No code suggestions found for the PR.')
|
||||||
|
pr_body = "## PR Code Suggestions ✨\n\nNo code suggestions found for the PR."
|
||||||
get_logger().debug(f"PR output", artifact=pr_body)
|
get_logger().debug(f"PR output", artifact=pr_body)
|
||||||
if self.progress_response:
|
if self.progress_response:
|
||||||
self.git_provider.edit_comment(self.progress_response, body=pr_body)
|
self.git_provider.edit_comment(self.progress_response, body=pr_body)
|
||||||
@ -156,8 +177,11 @@ class PRCodeSuggestions:
|
|||||||
self.push_inline_code_suggestions(data)
|
self.push_inline_code_suggestions(data)
|
||||||
if self.progress_response:
|
if self.progress_response:
|
||||||
self.progress_response.delete()
|
self.progress_response.delete()
|
||||||
|
else:
|
||||||
|
get_logger().info('Code suggestions generated for PR, but not published since publish_output is False.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
|
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
|
||||||
|
if get_settings().config.publish_output:
|
||||||
if self.progress_response:
|
if self.progress_response:
|
||||||
self.progress_response.delete()
|
self.progress_response.delete()
|
||||||
else:
|
else:
|
||||||
@ -174,6 +198,14 @@ class PRCodeSuggestions:
|
|||||||
final_update_message=True,
|
final_update_message=True,
|
||||||
max_previous_comments=4,
|
max_previous_comments=4,
|
||||||
progress_response=None):
|
progress_response=None):
|
||||||
|
|
||||||
|
if isinstance(self.git_provider, AzureDevopsProvider): # get_latest_commit_url is not supported yet
|
||||||
|
if progress_response:
|
||||||
|
self.git_provider.edit_comment(progress_response, pr_comment)
|
||||||
|
else:
|
||||||
|
self.git_provider.publish_comment(pr_comment)
|
||||||
|
return
|
||||||
|
|
||||||
history_header = f"#### Previous suggestions\n"
|
history_header = f"#### Previous suggestions\n"
|
||||||
last_commit_num = self.git_provider.get_latest_commit_url().split('/')[-1][:7]
|
last_commit_num = self.git_provider.get_latest_commit_url().split('/')[-1][:7]
|
||||||
latest_suggestion_header = f"Latest suggestions up to {last_commit_num}"
|
latest_suggestion_header = f"Latest suggestions up to {last_commit_num}"
|
||||||
@ -198,7 +230,8 @@ class PRCodeSuggestions:
|
|||||||
continue
|
continue
|
||||||
# find http link from comment.body[:table_index]
|
# find http link from comment.body[:table_index]
|
||||||
up_to_commit_txt = self.extract_link(comment.body[:table_index])
|
up_to_commit_txt = self.extract_link(comment.body[:table_index])
|
||||||
prev_suggestion_table = comment.body[table_index:comment.body.rfind("</table>") + len("</table>")]
|
prev_suggestion_table = comment.body[
|
||||||
|
table_index:comment.body.rfind("</table>") + len("</table>")]
|
||||||
|
|
||||||
tick = "✅ " if "✅" in prev_suggestion_table else ""
|
tick = "✅ " if "✅" in prev_suggestion_table else ""
|
||||||
# surround with details tag
|
# surround with details tag
|
||||||
@ -225,7 +258,8 @@ class PRCodeSuggestions:
|
|||||||
count += prev_suggestions.count(f"\n<details><summary>✅ {name.capitalize()}")
|
count += prev_suggestions.count(f"\n<details><summary>✅ {name.capitalize()}")
|
||||||
if count >= max_previous_comments:
|
if count >= max_previous_comments:
|
||||||
# remove the oldest suggestion
|
# remove the oldest suggestion
|
||||||
prev_suggestion_table = prev_suggestion_table[:prev_suggestion_table.rfind(f"<details><summary>{name.capitalize()} up to commit")]
|
prev_suggestion_table = prev_suggestion_table[:prev_suggestion_table.rfind(
|
||||||
|
f"<details><summary>{name.capitalize()} up to commit")]
|
||||||
|
|
||||||
tick = "✅ " if "✅" in latest_table else ""
|
tick = "✅ " if "✅" in latest_table else ""
|
||||||
# Add to the prev_suggestions section
|
# Add to the prev_suggestions section
|
||||||
@ -244,7 +278,7 @@ class PRCodeSuggestions:
|
|||||||
get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
|
get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
|
||||||
if progress_response: # publish to 'progress_response' comment, because it refreshes immediately
|
if progress_response: # publish to 'progress_response' comment, because it refreshes immediately
|
||||||
self.git_provider.edit_comment(progress_response, pr_comment_updated)
|
self.git_provider.edit_comment(progress_response, pr_comment_updated)
|
||||||
comment.delete()
|
self.git_provider.remove_comment(comment)
|
||||||
else:
|
else:
|
||||||
self.git_provider.edit_comment(comment, pr_comment_updated)
|
self.git_provider.edit_comment(comment, pr_comment_updated)
|
||||||
return
|
return
|
||||||
@ -274,13 +308,13 @@ class PRCodeSuggestions:
|
|||||||
self.token_handler,
|
self.token_handler,
|
||||||
model,
|
model,
|
||||||
add_line_numbers_to_hunks=True,
|
add_line_numbers_to_hunks=True,
|
||||||
disable_extra_lines=True)
|
disable_extra_lines=False)
|
||||||
|
|
||||||
if self.patches_diff:
|
if self.patches_diff:
|
||||||
get_logger().debug(f"PR diff", artifact=self.patches_diff)
|
get_logger().debug(f"PR diff", artifact=self.patches_diff)
|
||||||
self.prediction = await self._get_prediction(model, self.patches_diff)
|
self.prediction = await self._get_prediction(model, self.patches_diff)
|
||||||
else:
|
else:
|
||||||
get_logger().error(f"Error getting PR diff")
|
get_logger().warning(f"Empty PR diff")
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
|
|
||||||
data = self.prediction
|
data = self.prediction
|
||||||
@ -292,17 +326,17 @@ class PRCodeSuggestions:
|
|||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
system_prompt = environment.from_string(self.pr_code_suggestions_prompt_system).render(variables)
|
system_prompt = environment.from_string(self.pr_code_suggestions_prompt_system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
system=system_prompt, user=user_prompt)
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
|
|
||||||
# load suggestions from the AI response
|
# load suggestions from the AI response
|
||||||
data = self._prepare_pr_code_suggestions(response)
|
data = self._prepare_pr_code_suggestions(response)
|
||||||
|
|
||||||
# self-reflect on suggestions
|
# self-reflect on suggestions
|
||||||
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
model = get_settings().config.model_turbo # use turbo model for self-reflection, since it is an easier task
|
model_turbo = get_settings().config.model_turbo # use turbo model for self-reflection, since it is an easier task
|
||||||
response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"], patches_diff,
|
response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"],
|
||||||
model=model)
|
patches_diff, model=model_turbo)
|
||||||
if response_reflect:
|
if response_reflect:
|
||||||
response_reflect_yaml = load_yaml(response_reflect)
|
response_reflect_yaml = load_yaml(response_reflect)
|
||||||
code_suggestions_feedback = response_reflect_yaml["code_suggestions"]
|
code_suggestions_feedback = response_reflect_yaml["code_suggestions"]
|
||||||
@ -349,12 +383,14 @@ class PRCodeSuggestions:
|
|||||||
one_sentence_summary_list = []
|
one_sentence_summary_list = []
|
||||||
for i, suggestion in enumerate(data['code_suggestions']):
|
for i, suggestion in enumerate(data['code_suggestions']):
|
||||||
try:
|
try:
|
||||||
needed_keys = ['one_sentence_summary', 'label', 'relevant_file', 'relevant_lines_start', 'relevant_lines_end']
|
needed_keys = ['one_sentence_summary', 'label', 'relevant_file', 'relevant_lines_start',
|
||||||
|
'relevant_lines_end']
|
||||||
is_valid_keys = True
|
is_valid_keys = True
|
||||||
for key in needed_keys:
|
for key in needed_keys:
|
||||||
if key not in suggestion:
|
if key not in suggestion:
|
||||||
is_valid_keys = False
|
is_valid_keys = False
|
||||||
get_logger().debug(f"Skipping suggestion {i + 1}, because it does not contain '{key}':\n'{suggestion}")
|
get_logger().debug(
|
||||||
|
f"Skipping suggestion {i + 1}, because it does not contain '{key}':\n'{suggestion}")
|
||||||
break
|
break
|
||||||
if not is_valid_keys:
|
if not is_valid_keys:
|
||||||
continue
|
continue
|
||||||
@ -420,7 +456,8 @@ class PRCodeSuggestions:
|
|||||||
body = f"**Suggestion:** {content} [{label}]\n```suggestion\n" + new_code_snippet + "\n```"
|
body = f"**Suggestion:** {content} [{label}]\n```suggestion\n" + new_code_snippet + "\n```"
|
||||||
code_suggestions.append({'body': body, 'relevant_file': relevant_file,
|
code_suggestions.append({'body': body, 'relevant_file': relevant_file,
|
||||||
'relevant_lines_start': relevant_lines_start,
|
'relevant_lines_start': relevant_lines_start,
|
||||||
'relevant_lines_end': relevant_lines_end})
|
'relevant_lines_end': relevant_lines_end,
|
||||||
|
'original_suggestion': d})
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger().info(f"Could not parse suggestion: {d}")
|
get_logger().info(f"Could not parse suggestion: {d}")
|
||||||
|
|
||||||
@ -437,8 +474,24 @@ class PRCodeSuggestions:
|
|||||||
original_initial_line = None
|
original_initial_line = None
|
||||||
for file in self.diff_files:
|
for file in self.diff_files:
|
||||||
if file.filename.strip() == relevant_file:
|
if file.filename.strip() == relevant_file:
|
||||||
if file.head_file: # in bitbucket, head_file is empty. toDo: fix this
|
if file.head_file:
|
||||||
original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1]
|
file_lines = file.head_file.splitlines()
|
||||||
|
if relevant_lines_start > len(file_lines):
|
||||||
|
get_logger().warning(
|
||||||
|
"Could not dedent code snippet, because relevant_lines_start is out of range",
|
||||||
|
artifact={'filename': file.filename,
|
||||||
|
'file_content': file.head_file,
|
||||||
|
'relevant_lines_start': relevant_lines_start,
|
||||||
|
'new_code_snippet': new_code_snippet})
|
||||||
|
return new_code_snippet
|
||||||
|
else:
|
||||||
|
original_initial_line = file_lines[relevant_lines_start - 1]
|
||||||
|
else:
|
||||||
|
get_logger().warning("Could not dedent code snippet, because head_file is missing",
|
||||||
|
artifact={'filename': file.filename,
|
||||||
|
'relevant_lines_start': relevant_lines_start,
|
||||||
|
'new_code_snippet': new_code_snippet})
|
||||||
|
return new_code_snippet
|
||||||
break
|
break
|
||||||
if original_initial_line:
|
if original_initial_line:
|
||||||
suggested_initial_line = new_code_snippet.splitlines()[0]
|
suggested_initial_line = new_code_snippet.splitlines()[0]
|
||||||
@ -448,7 +501,7 @@ class PRCodeSuggestions:
|
|||||||
if delta_spaces > 0:
|
if delta_spaces > 0:
|
||||||
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
|
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
|
get_logger().error(f"Error when dedenting code snippet for file {relevant_file}, error: {e}")
|
||||||
|
|
||||||
return new_code_snippet
|
return new_code_snippet
|
||||||
|
|
||||||
@ -458,7 +511,7 @@ class PRCodeSuggestions:
|
|||||||
get_logger().info("Extended mode is enabled by the `--extended` flag")
|
get_logger().info("Extended mode is enabled by the `--extended` flag")
|
||||||
return True
|
return True
|
||||||
if get_settings().pr_code_suggestions.auto_extended_mode:
|
if get_settings().pr_code_suggestions.auto_extended_mode:
|
||||||
get_logger().info("Extended mode is enabled automatically based on the configuration toggle")
|
# get_logger().info("Extended mode is enabled automatically based on the configuration toggle")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -483,11 +536,11 @@ class PRCodeSuggestions:
|
|||||||
data = {"code_suggestions": []}
|
data = {"code_suggestions": []}
|
||||||
for j, predictions in enumerate(prediction_list): # each call adds an element to the list
|
for j, predictions in enumerate(prediction_list): # each call adds an element to the list
|
||||||
if "code_suggestions" in predictions:
|
if "code_suggestions" in predictions:
|
||||||
score_threshold = max(1, get_settings().pr_code_suggestions.suggestions_score_threshold)
|
score_threshold = max(1, int(get_settings().pr_code_suggestions.suggestions_score_threshold))
|
||||||
for i, prediction in enumerate(predictions["code_suggestions"]):
|
for i, prediction in enumerate(predictions["code_suggestions"]):
|
||||||
try:
|
try:
|
||||||
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
score = int(prediction["score"])
|
score = int(prediction.get("score", 1))
|
||||||
if score >= score_threshold:
|
if score >= score_threshold:
|
||||||
data["code_suggestions"].append(prediction)
|
data["code_suggestions"].append(prediction)
|
||||||
else:
|
else:
|
||||||
@ -500,7 +553,7 @@ class PRCodeSuggestions:
|
|||||||
get_logger().error(f"Error getting PR diff for suggestion {i} in call {j}, error: {e}")
|
get_logger().error(f"Error getting PR diff for suggestion {i} in call {j}, error: {e}")
|
||||||
self.data = data
|
self.data = data
|
||||||
else:
|
else:
|
||||||
get_logger().error(f"Error getting PR diff")
|
get_logger().warning(f"Empty PR diff list")
|
||||||
self.data = data = None
|
self.data = data = None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -624,9 +677,9 @@ class PRCodeSuggestions:
|
|||||||
code_snippet_link = ""
|
code_snippet_link = ""
|
||||||
# add html table for each suggestion
|
# add html table for each suggestion
|
||||||
|
|
||||||
suggestion_content = suggestion['suggestion_content'].rstrip().rstrip()
|
suggestion_content = suggestion['suggestion_content'].rstrip()
|
||||||
|
CHAR_LIMIT_PER_LINE = 84
|
||||||
suggestion_content = insert_br_after_x_chars(suggestion_content, 90)
|
suggestion_content = insert_br_after_x_chars(suggestion_content, CHAR_LIMIT_PER_LINE)
|
||||||
# pr_body += f"<tr><td><details><summary>{suggestion_content}</summary>"
|
# pr_body += f"<tr><td><details><summary>{suggestion_content}</summary>"
|
||||||
existing_code = suggestion['existing_code'].rstrip() + "\n"
|
existing_code = suggestion['existing_code'].rstrip() + "\n"
|
||||||
improved_code = suggestion['improved_code'].rstrip() + "\n"
|
improved_code = suggestion['improved_code'].rstrip() + "\n"
|
||||||
@ -643,6 +696,11 @@ class PRCodeSuggestions:
|
|||||||
else:
|
else:
|
||||||
pr_body += f"""<tr><td>\n\n"""
|
pr_body += f"""<tr><td>\n\n"""
|
||||||
suggestion_summary = suggestion['one_sentence_summary'].strip().rstrip('.')
|
suggestion_summary = suggestion['one_sentence_summary'].strip().rstrip('.')
|
||||||
|
if "'<" in suggestion_summary and ">'" in suggestion_summary:
|
||||||
|
# escape the '<' and '>' characters, otherwise they are interpreted as html tags
|
||||||
|
get_logger().info(f"Escaped suggestion summary: {suggestion_summary}")
|
||||||
|
suggestion_summary = suggestion_summary.replace("'<", "`<")
|
||||||
|
suggestion_summary = suggestion_summary.replace(">'", ">`")
|
||||||
if '`' in suggestion_summary:
|
if '`' in suggestion_summary:
|
||||||
suggestion_summary = replace_code_tags(suggestion_summary)
|
suggestion_summary = replace_code_tags(suggestion_summary)
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from dynaconf import Dynaconf
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
@ -28,20 +30,33 @@ class PRConfig:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _prepare_pr_configs(self) -> str:
|
def _prepare_pr_configs(self) -> str:
|
||||||
import tomli
|
conf_file = get_settings().find_file("configuration.toml")
|
||||||
with open(get_settings().find_file("configuration.toml"), "rb") as conf_file:
|
conf_settings = Dynaconf(settings_files=[conf_file])
|
||||||
configuration_headers = [header.lower() for header in tomli.load(conf_file).keys()]
|
configuration_headers = [header.lower() for header in conf_settings.keys()]
|
||||||
relevant_configs = {
|
relevant_configs = {
|
||||||
header: configs for header, configs in get_settings().to_dict().items()
|
header: configs for header, configs in get_settings().to_dict().items()
|
||||||
if header.lower().startswith("pr_") and header.lower() in configuration_headers
|
if (header.lower().startswith("pr_") or header.lower().startswith("config")) and header.lower() in configuration_headers
|
||||||
}
|
}
|
||||||
comment_str = "Possible Configurations:"
|
|
||||||
|
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']
|
||||||
|
extra_skip_keys = get_settings().config.get('config.skip_keys', [])
|
||||||
|
if extra_skip_keys:
|
||||||
|
skip_keys.extend(extra_skip_keys)
|
||||||
|
|
||||||
|
markdown_text = "<details> <summary><strong>🛠️ PR-Agent Configurations:</strong></summary> \n\n"
|
||||||
|
markdown_text += f"\n\n```yaml\n\n"
|
||||||
for header, configs in relevant_configs.items():
|
for header, configs in relevant_configs.items():
|
||||||
if configs:
|
if configs:
|
||||||
comment_str += "\n"
|
markdown_text += "\n\n"
|
||||||
|
markdown_text += f"==================== {header} ===================="
|
||||||
for key, value in configs.items():
|
for key, value in configs.items():
|
||||||
comment_str += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
|
if key in skip_keys:
|
||||||
comment_str += " "
|
continue
|
||||||
if get_settings().config.verbosity_level >= 2:
|
markdown_text += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
|
||||||
get_logger().info(f"comment_str:\n{comment_str}")
|
markdown_text += " "
|
||||||
return comment_str
|
markdown_text += "\n```"
|
||||||
|
markdown_text += "\n</details>\n"
|
||||||
|
get_logger().info(f"Possible Configurations outputted to PR comment", artifact=markdown_text)
|
||||||
|
return markdown_text
|
||||||
|
@ -92,7 +92,7 @@ class PRDescription:
|
|||||||
if self.prediction:
|
if self.prediction:
|
||||||
self._prepare_data()
|
self._prepare_data()
|
||||||
else:
|
else:
|
||||||
get_logger().error(f"Error getting AI prediction {self.pr_id}")
|
get_logger().warning(f"Empty prediction, PR: {self.pr_id}")
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ class PRDescription:
|
|||||||
pr_body += "<hr>\n\n<details> <summary><strong>✨ Describe tool usage guide:</strong></summary><hr> \n\n"
|
pr_body += "<hr>\n\n<details> <summary><strong>✨ Describe tool usage guide:</strong></summary><hr> \n\n"
|
||||||
pr_body += HelpMessage.get_describe_usage_guide()
|
pr_body += HelpMessage.get_describe_usage_guide()
|
||||||
pr_body += "\n</details>\n"
|
pr_body += "\n</details>\n"
|
||||||
elif get_settings().pr_description.enable_help_comment:
|
elif self.git_provider.is_supported("gfm_markdown") and get_settings().pr_description.enable_help_comment:
|
||||||
pr_body += "\n\n___\n\n> 💡 **PR-Agent usage**:"
|
pr_body += "\n\n___\n\n> 💡 **PR-Agent usage**:"
|
||||||
pr_body += "\n>Comment `/help` on the PR to get a list of all available PR-Agent tools and their descriptions\n\n"
|
pr_body += "\n>Comment `/help` on the PR to get a list of all available PR-Agent tools and their descriptions\n\n"
|
||||||
|
|
||||||
@ -167,10 +167,13 @@ class PRDescription:
|
|||||||
|
|
||||||
async def _prepare_prediction(self, model: str) -> None:
|
async def _prepare_prediction(self, model: str) -> None:
|
||||||
if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
|
if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
|
||||||
|
get_logger().info(
|
||||||
|
"Markers were enabled, but user description does not contain markers. skipping AI prediction")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
large_pr_handling = get_settings().pr_description.enable_large_pr_handling and "pr_description_only_files_prompts" in get_settings()
|
large_pr_handling = get_settings().pr_description.enable_large_pr_handling and "pr_description_only_files_prompts" in get_settings()
|
||||||
output = get_pr_diff(self.git_provider, self.token_handler, model, large_pr_handling=large_pr_handling, return_remaining_files=True)
|
output = get_pr_diff(self.git_provider, self.token_handler, model, large_pr_handling=large_pr_handling,
|
||||||
|
return_remaining_files=True)
|
||||||
if isinstance(output, tuple):
|
if isinstance(output, tuple):
|
||||||
patches_diff, remaining_files_list = output
|
patches_diff, remaining_files_list = output
|
||||||
else:
|
else:
|
||||||
@ -213,6 +216,7 @@ class PRDescription:
|
|||||||
else: # async calls
|
else: # async calls
|
||||||
tasks = []
|
tasks = []
|
||||||
for i, patches in enumerate(patches_compressed_list):
|
for i, patches in enumerate(patches_compressed_list):
|
||||||
|
if patches:
|
||||||
patches_diff = "\n".join(patches)
|
patches_diff = "\n".join(patches)
|
||||||
get_logger().debug(f"PR diff number {i + 1} for describe files")
|
get_logger().debug(f"PR diff number {i + 1} for describe files")
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
@ -237,14 +241,23 @@ class PRDescription:
|
|||||||
get_settings().pr_description_only_description_prompts.user)
|
get_settings().pr_description_only_description_prompts.user)
|
||||||
files_walkthrough = "\n".join(file_description_str_list)
|
files_walkthrough = "\n".join(file_description_str_list)
|
||||||
files_walkthrough_prompt = copy.deepcopy(files_walkthrough)
|
files_walkthrough_prompt = copy.deepcopy(files_walkthrough)
|
||||||
|
MAX_EXTRA_FILES_TO_PROMPT = 50
|
||||||
if remaining_files_list:
|
if remaining_files_list:
|
||||||
files_walkthrough_prompt += "\n\nNo more token budget. Additional unprocessed files:"
|
files_walkthrough_prompt += "\n\nNo more token budget. Additional unprocessed files:"
|
||||||
for file in remaining_files_list:
|
for i, file in enumerate(remaining_files_list):
|
||||||
files_walkthrough_prompt += f"\n- {file}"
|
files_walkthrough_prompt += f"\n- {file}"
|
||||||
|
if i >= MAX_EXTRA_FILES_TO_PROMPT:
|
||||||
|
get_logger().debug(f"Too many remaining files, clipping to {MAX_EXTRA_FILES_TO_PROMPT}")
|
||||||
|
files_walkthrough_prompt += f"\n... and {len(remaining_files_list) - MAX_EXTRA_FILES_TO_PROMPT} more"
|
||||||
|
break
|
||||||
if deleted_files_list:
|
if deleted_files_list:
|
||||||
files_walkthrough_prompt += "\n\nAdditional deleted files:"
|
files_walkthrough_prompt += "\n\nAdditional deleted files:"
|
||||||
for file in deleted_files_list:
|
for i, file in enumerate(deleted_files_list):
|
||||||
files_walkthrough_prompt += f"\n- {file}"
|
files_walkthrough_prompt += f"\n- {file}"
|
||||||
|
if i >= MAX_EXTRA_FILES_TO_PROMPT:
|
||||||
|
get_logger().debug(f"Too many deleted files, clipping to {MAX_EXTRA_FILES_TO_PROMPT}")
|
||||||
|
files_walkthrough_prompt += f"\n... and {len(deleted_files_list) - MAX_EXTRA_FILES_TO_PROMPT} more"
|
||||||
|
break
|
||||||
tokens_files_walkthrough = len(
|
tokens_files_walkthrough = len(
|
||||||
token_handler_only_description_prompt.encoder.encode(files_walkthrough_prompt))
|
token_handler_only_description_prompt.encoder.encode(files_walkthrough_prompt))
|
||||||
total_tokens = token_handler_only_description_prompt.prompt_tokens + tokens_files_walkthrough
|
total_tokens = token_handler_only_description_prompt.prompt_tokens + tokens_files_walkthrough
|
||||||
@ -262,8 +275,9 @@ class PRDescription:
|
|||||||
prediction_headers = prediction_headers.strip().removeprefix('```yaml').strip('`').strip()
|
prediction_headers = prediction_headers.strip().removeprefix('```yaml').strip('`').strip()
|
||||||
|
|
||||||
# manually add extra files to final prediction
|
# manually add extra files to final prediction
|
||||||
|
MAX_EXTRA_FILES_TO_OUTPUT = 100
|
||||||
if get_settings().pr_description.mention_extra_files:
|
if get_settings().pr_description.mention_extra_files:
|
||||||
for file in remaining_files_list:
|
for i, file in enumerate(remaining_files_list):
|
||||||
extra_file_yaml = f"""\
|
extra_file_yaml = f"""\
|
||||||
- filename: |
|
- filename: |
|
||||||
{file}
|
{file}
|
||||||
@ -275,6 +289,20 @@ class PRDescription:
|
|||||||
additional files (token-limit)
|
additional files (token-limit)
|
||||||
"""
|
"""
|
||||||
files_walkthrough = files_walkthrough.strip() + "\n" + extra_file_yaml.strip()
|
files_walkthrough = files_walkthrough.strip() + "\n" + extra_file_yaml.strip()
|
||||||
|
if i >= MAX_EXTRA_FILES_TO_OUTPUT:
|
||||||
|
files_walkthrough += f"""\
|
||||||
|
extra_file_yaml =
|
||||||
|
- filename: |
|
||||||
|
Additional {len(remaining_files_list) - MAX_EXTRA_FILES_TO_OUTPUT} files not shown
|
||||||
|
changes_summary: |
|
||||||
|
...
|
||||||
|
changes_title: |
|
||||||
|
...
|
||||||
|
label: |
|
||||||
|
additional files (token-limit)
|
||||||
|
"""
|
||||||
|
break
|
||||||
|
|
||||||
# final processing
|
# final processing
|
||||||
self.prediction = prediction_headers + "\n" + "pr_files:\n" + files_walkthrough
|
self.prediction = prediction_headers + "\n" + "pr_files:\n" + files_walkthrough
|
||||||
if not load_yaml(self.prediction):
|
if not load_yaml(self.prediction):
|
||||||
@ -320,12 +348,12 @@ class PRDescription:
|
|||||||
set_custom_labels(variables, self.git_provider)
|
set_custom_labels(variables, self.git_provider)
|
||||||
self.variables = variables
|
self.variables = variables
|
||||||
|
|
||||||
system_prompt = environment.from_string(get_settings().get(prompt, {}).get("system", "")).render(variables)
|
system_prompt = environment.from_string(get_settings().get(prompt, {}).get("system", "")).render(self.variables)
|
||||||
user_prompt = environment.from_string(get_settings().get(prompt, {}).get("user", "")).render(variables)
|
user_prompt = environment.from_string(get_settings().get(prompt, {}).get("user", "")).render(self.variables)
|
||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model,
|
model=model,
|
||||||
temperature=0.2,
|
temperature=get_settings().config.temperature,
|
||||||
system=system_prompt,
|
system=system_prompt,
|
||||||
user=user_prompt
|
user=user_prompt
|
||||||
)
|
)
|
||||||
@ -367,6 +395,7 @@ class PRDescription:
|
|||||||
pr_types = self.data['type']
|
pr_types = self.data['type']
|
||||||
elif type(self.data['type']) == str:
|
elif type(self.data['type']) == str:
|
||||||
pr_types = self.data['type'].split(',')
|
pr_types = self.data['type'].split(',')
|
||||||
|
pr_types = [label.strip() for label in pr_types]
|
||||||
|
|
||||||
# convert lowercase labels to original case
|
# convert lowercase labels to original case
|
||||||
try:
|
try:
|
||||||
@ -464,13 +493,13 @@ class PRDescription:
|
|||||||
pr_body += f'- `{filename}`: {description}\n'
|
pr_body += f'- `{filename}`: {description}\n'
|
||||||
if self.git_provider.is_supported("gfm_markdown"):
|
if self.git_provider.is_supported("gfm_markdown"):
|
||||||
pr_body += "</details>\n"
|
pr_body += "</details>\n"
|
||||||
elif 'pr_files' in key.lower():
|
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types:
|
||||||
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
||||||
changes_walkthrough = f"### **Changes walkthrough** 📝\n{changes_walkthrough}"
|
changes_walkthrough = f"### **Changes walkthrough** 📝\n{changes_walkthrough}"
|
||||||
else:
|
else:
|
||||||
# if the value is a list, join its items by comma
|
# if the value is a list, join its items by comma
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
value = ', '.join(v for v in value)
|
value = ', '.join(v.rstrip() for v in value)
|
||||||
pr_body += f"{value}\n"
|
pr_body += f"{value}\n"
|
||||||
if idx < len(self.data) - 1:
|
if idx < len(self.data) - 1:
|
||||||
pr_body += "\n\n___\n\n"
|
pr_body += "\n\n___\n\n"
|
||||||
@ -479,8 +508,17 @@ class PRDescription:
|
|||||||
|
|
||||||
def _prepare_file_labels(self):
|
def _prepare_file_labels(self):
|
||||||
file_label_dict = {}
|
file_label_dict = {}
|
||||||
|
if (not self.data or not isinstance(self.data, dict) or
|
||||||
|
'pr_files' not in self.data or not self.data['pr_files']):
|
||||||
|
return file_label_dict
|
||||||
for file in self.data['pr_files']:
|
for file in self.data['pr_files']:
|
||||||
try:
|
try:
|
||||||
|
required_fields = ['changes_summary', 'changes_title', 'filename', 'label']
|
||||||
|
if not all(field in file for field in required_fields):
|
||||||
|
# can happen for example if a YAML generation was interrupted in the middle (no more tokens)
|
||||||
|
get_logger().warning(f"Missing required fields in file label dict {self.pr_id}, skipping file",
|
||||||
|
artifact={"file": file})
|
||||||
|
continue
|
||||||
filename = file['filename'].replace("'", "`").replace('"', '`')
|
filename = file['filename'].replace("'", "`").replace('"', '`')
|
||||||
changes_summary = file['changes_summary']
|
changes_summary = file['changes_summary']
|
||||||
changes_title = file['changes_title'].strip()
|
changes_title = file['changes_title'].strip()
|
||||||
@ -505,7 +543,7 @@ class PRDescription:
|
|||||||
use_collapsible_file_list = num_files > self.COLLAPSIBLE_FILE_LIST_THRESHOLD
|
use_collapsible_file_list = num_files > self.COLLAPSIBLE_FILE_LIST_THRESHOLD
|
||||||
|
|
||||||
if not self.git_provider.is_supported("gfm_markdown"):
|
if not self.git_provider.is_supported("gfm_markdown"):
|
||||||
return pr_body
|
return pr_body, pr_comments
|
||||||
try:
|
try:
|
||||||
pr_body += "<table>"
|
pr_body += "<table>"
|
||||||
header = f"Relevant files"
|
header = f"Relevant files"
|
||||||
@ -600,9 +638,10 @@ def insert_br_after_x_chars(text, x=70):
|
|||||||
text = replace_code_tags(text)
|
text = replace_code_tags(text)
|
||||||
|
|
||||||
# convert list items to <li>
|
# convert list items to <li>
|
||||||
if text.startswith("- "):
|
if text.startswith("- ") or text.startswith("* "):
|
||||||
text = "<li>" + text[2:]
|
text = "<li>" + text[2:]
|
||||||
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
|
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
|
||||||
|
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
|
||||||
|
|
||||||
# convert new lines to <br>
|
# convert new lines to <br>
|
||||||
text = text.replace("\n", '<br>')
|
text = text.replace("\n", '<br>')
|
||||||
|
@ -137,12 +137,13 @@ class PRGenerateLabels:
|
|||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
set_custom_labels(variables, self.git_provider)
|
set_custom_labels(variables, self.git_provider)
|
||||||
self.variables = variables
|
self.variables = variables
|
||||||
system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables)
|
|
||||||
user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(self.variables)
|
||||||
|
user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(self.variables)
|
||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model,
|
model=model,
|
||||||
temperature=0.2,
|
temperature=get_settings().config.temperature,
|
||||||
system=system_prompt,
|
system=system_prompt,
|
||||||
user=user_prompt
|
user=user_prompt
|
||||||
)
|
)
|
||||||
@ -164,6 +165,7 @@ class PRGenerateLabels:
|
|||||||
pr_types = self.data['labels']
|
pr_types = self.data['labels']
|
||||||
elif type(self.data['labels']) == str:
|
elif type(self.data['labels']) == str:
|
||||||
pr_types = self.data['labels'].split(',')
|
pr_types = self.data['labels'].split(',')
|
||||||
|
pr_types = [label.strip() for label in pr_types]
|
||||||
|
|
||||||
# convert lowercase labels to original case
|
# convert lowercase labels to original case
|
||||||
try:
|
try:
|
||||||
|
@ -66,8 +66,8 @@ class PRInformationFromUser:
|
|||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
system=system_prompt, user=user_prompt)
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_pr_answer(self) -> str:
|
def _prepare_pr_answer(self) -> str:
|
||||||
|
@ -102,6 +102,6 @@ class PR_LineQuestions:
|
|||||||
print(f"\nSystem prompt:\n{system_prompt}")
|
print(f"\nSystem prompt:\n{system_prompt}")
|
||||||
print(f"\nUser prompt:\n{user_prompt}")
|
print(f"\nUser prompt:\n{user_prompt}")
|
||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
system=system_prompt, user=user_prompt)
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
return response
|
return response
|
||||||
|
@ -108,12 +108,12 @@ class PRQuestions:
|
|||||||
user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables)
|
||||||
if 'img_path' in variables:
|
if 'img_path' in variables:
|
||||||
img_path = self.vars['img_path']
|
img_path = self.vars['img_path']
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await (self.ai_handler.chat_completion
|
||||||
system=system_prompt, user=user_prompt,
|
(model=model, temperature=get_settings().config.temperature,
|
||||||
img_path=img_path)
|
system=system_prompt, user=user_prompt, img_path=img_path))
|
||||||
else:
|
else:
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
system=system_prompt, user=user_prompt)
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_pr_answer(self) -> str:
|
def _prepare_pr_answer(self) -> str:
|
||||||
|
@ -6,7 +6,7 @@ from typing import List, Tuple
|
|||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
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.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, add_ai_metadata_to_diff_files
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import github_action_output, load_yaml, ModelType, \
|
from pr_agent.algo.utils import github_action_output, load_yaml, ModelType, \
|
||||||
show_relevant_configurations, convert_to_markdown_v2, PRReviewHeader
|
show_relevant_configurations, convert_to_markdown_v2, PRReviewHeader
|
||||||
@ -51,15 +51,23 @@ class PRReviewer:
|
|||||||
raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now")
|
raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now")
|
||||||
self.ai_handler = ai_handler()
|
self.ai_handler = ai_handler()
|
||||||
self.ai_handler.main_pr_language = self.main_language
|
self.ai_handler.main_pr_language = self.main_language
|
||||||
|
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
|
|
||||||
answer_str, question_str = self._get_user_answers()
|
answer_str, question_str = self._get_user_answers()
|
||||||
|
self.pr_description, self.pr_description_files = (
|
||||||
|
self.git_provider.get_pr_description(split_changes_walkthrough=True))
|
||||||
|
if (self.pr_description_files and get_settings().get("config.is_auto_command", False) and
|
||||||
|
get_settings().get("config.enable_ai_metadata", False)):
|
||||||
|
add_ai_metadata_to_diff_files(self.git_provider, self.pr_description_files)
|
||||||
|
get_logger().debug(f"AI metadata added to the this command")
|
||||||
|
else:
|
||||||
|
get_settings().set("config.enable_ai_metadata", False)
|
||||||
|
get_logger().debug(f"AI metadata is disabled for this command")
|
||||||
|
|
||||||
self.vars = {
|
self.vars = {
|
||||||
"title": self.git_provider.pr.title,
|
"title": self.git_provider.pr.title,
|
||||||
"branch": self.git_provider.get_pr_branch(),
|
"branch": self.git_provider.get_pr_branch(),
|
||||||
"description": self.git_provider.get_pr_description(),
|
"description": self.pr_description,
|
||||||
"language": self.main_language,
|
"language": self.main_language,
|
||||||
"diff": "", # empty diff for initial calculation
|
"diff": "", # empty diff for initial calculation
|
||||||
"num_pr_files": self.git_provider.get_num_of_files(),
|
"num_pr_files": self.git_provider.get_num_of_files(),
|
||||||
@ -75,6 +83,7 @@ class PRReviewer:
|
|||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
"custom_labels": "",
|
"custom_labels": "",
|
||||||
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
||||||
|
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.token_handler = TokenHandler(
|
self.token_handler = TokenHandler(
|
||||||
@ -95,6 +104,10 @@ class PRReviewer:
|
|||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
if not self.git_provider.get_files():
|
||||||
|
get_logger().info(f"PR has no files: {self.pr_url}, skipping review")
|
||||||
|
return None
|
||||||
|
|
||||||
if self.incremental.is_incremental and not self._can_run_incremental_review():
|
if self.incremental.is_incremental and not self._can_run_incremental_review():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -147,12 +160,17 @@ class PRReviewer:
|
|||||||
get_logger().error(f"Failed to review PR: {e}")
|
get_logger().error(f"Failed to review PR: {e}")
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str) -> None:
|
async def _prepare_prediction(self, model: str) -> None:
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
self.patches_diff = get_pr_diff(self.git_provider,
|
||||||
|
self.token_handler,
|
||||||
|
model,
|
||||||
|
add_line_numbers_to_hunks=True,
|
||||||
|
disable_extra_lines=False,)
|
||||||
|
|
||||||
if self.patches_diff:
|
if self.patches_diff:
|
||||||
get_logger().debug(f"PR diff", diff=self.patches_diff)
|
get_logger().debug(f"PR diff", diff=self.patches_diff)
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
else:
|
else:
|
||||||
get_logger().error(f"Error getting PR diff")
|
get_logger().warning(f"Empty diff for PR: {self.pr_url}")
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
|
|
||||||
async def _get_prediction(self, model: str) -> str:
|
async def _get_prediction(self, model: str) -> str:
|
||||||
@ -174,7 +192,7 @@ class PRReviewer:
|
|||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model,
|
model=model,
|
||||||
temperature=0.2,
|
temperature=get_settings().config.temperature,
|
||||||
system=system_prompt,
|
system=system_prompt,
|
||||||
user=user_prompt
|
user=user_prompt
|
||||||
)
|
)
|
||||||
@ -234,7 +252,7 @@ class PRReviewer:
|
|||||||
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
|
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
|
||||||
|
|
||||||
markdown_text = convert_to_markdown_v2(data, self.git_provider.is_supported("gfm_markdown"),
|
markdown_text = convert_to_markdown_v2(data, self.git_provider.is_supported("gfm_markdown"),
|
||||||
incremental_review_markdown_text)
|
incremental_review_markdown_text, git_provider=self.git_provider)
|
||||||
|
|
||||||
# Add help text if gfm_markdown is supported
|
# Add help text if gfm_markdown is supported
|
||||||
if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_reviewer.enable_help_text:
|
if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_reviewer.enable_help_text:
|
||||||
@ -281,7 +299,7 @@ class PRReviewer:
|
|||||||
if comment:
|
if comment:
|
||||||
comments.append(comment)
|
comments.append(comment)
|
||||||
else:
|
else:
|
||||||
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
|
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file, suggestion)
|
||||||
|
|
||||||
if comments:
|
if comments:
|
||||||
self.git_provider.publish_inline_comments(comments)
|
self.git_provider.publish_inline_comments(comments)
|
||||||
@ -372,6 +390,11 @@ class PRReviewer:
|
|||||||
if not get_settings().config.publish_output:
|
if not get_settings().config.publish_output:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not get_settings().pr_reviewer.require_estimate_effort_to_review:
|
||||||
|
get_settings().pr_reviewer.enable_review_labels_effort = False # we did not generate this output
|
||||||
|
if not get_settings().pr_reviewer.require_security_review:
|
||||||
|
get_settings().pr_reviewer.enable_review_labels_security = False # we did not generate this output
|
||||||
|
|
||||||
if (get_settings().pr_reviewer.enable_review_labels_security or
|
if (get_settings().pr_reviewer.enable_review_labels_security or
|
||||||
get_settings().pr_reviewer.enable_review_labels_effort):
|
get_settings().pr_reviewer.enable_review_labels_effort):
|
||||||
try:
|
try:
|
||||||
|
@ -103,8 +103,8 @@ class PRUpdateChangelog:
|
|||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
system=system_prompt, user=user_prompt)
|
model=model, system=system_prompt, user=user_prompt, temperature=get_settings().config.temperature)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ class PRUpdateChangelog:
|
|||||||
file_path="CHANGELOG.md",
|
file_path="CHANGELOG.md",
|
||||||
branch=self.git_provider.get_pr_branch(),
|
branch=self.git_provider.get_pr_branch(),
|
||||||
contents=new_file_content,
|
contents=new_file_content,
|
||||||
message="Update CHANGELOG.md",
|
message="[skip ci] Update CHANGELOG.md",
|
||||||
)
|
)
|
||||||
|
|
||||||
sleep(5) # wait for the file to be updated
|
sleep(5) # wait for the file to be updated
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pr-agent"
|
name = "pr-agent"
|
||||||
version = "0.2.2"
|
version = "0.2.4"
|
||||||
|
|
||||||
authors = [{name= "CodiumAI", email = "tal.r@codium.ai"}]
|
authors = [{name= "CodiumAI", email = "tal.r@codium.ai"}]
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
aiohttp==3.9.1
|
aiohttp==3.9.4
|
||||||
anthropic[vertex]==0.21.3
|
anthropic[vertex]==0.21.3
|
||||||
atlassian-python-api==3.41.4
|
atlassian-python-api==3.41.4
|
||||||
azure-devops==7.1.0b3
|
azure-devops==7.1.0b3
|
||||||
@ -6,14 +6,14 @@ azure-identity==1.15.0
|
|||||||
boto3==1.33.6
|
boto3==1.33.6
|
||||||
dynaconf==3.2.4
|
dynaconf==3.2.4
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
GitPython==3.1.32
|
GitPython==3.1.41
|
||||||
google-cloud-aiplatform==1.38.0
|
google-cloud-aiplatform==1.38.0
|
||||||
google-cloud-storage==2.10.0
|
google-cloud-storage==2.10.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
litellm==1.40.17
|
litellm==1.43.13
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
msrest==0.7.1
|
msrest==0.7.1
|
||||||
openai==1.35.1
|
openai==1.40.6
|
||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
PyGithub==1.59.*
|
PyGithub==1.59.*
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
@ -24,10 +24,15 @@ tiktoken==0.7.0
|
|||||||
ujson==5.8.0
|
ujson==5.8.0
|
||||||
uvicorn==0.22.0
|
uvicorn==0.22.0
|
||||||
tenacity==8.2.3
|
tenacity==8.2.3
|
||||||
gunicorn==20.1.0
|
gunicorn==22.0.0
|
||||||
|
pytest-cov==5.0.0
|
||||||
|
pydantic==2.8.2
|
||||||
|
html2text==2024.2.26
|
||||||
# Uncomment the following lines to enable the 'similar issue' tool
|
# Uncomment the following lines to enable the 'similar issue' tool
|
||||||
# pinecone-client
|
# pinecone-client
|
||||||
# pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
|
# pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
|
||||||
# lancedb==0.5.1
|
# lancedb==0.5.1
|
||||||
# uncomment this to support language LangChainOpenAIHandler
|
# uncomment this to support language LangChainOpenAIHandler
|
||||||
# langchain==0.0.349
|
# langchain==0.2.0
|
||||||
|
# langchain-core==0.2.28
|
||||||
|
# langchain-openai==0.1.20
|
||||||
|
1
setup.py
1
setup.py
@ -3,3 +3,4 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
print("aaa")
|
||||||
|
35
tests/e2e_tests/e2e_utils.py
Normal file
35
tests/e2e_tests/e2e_utils.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
FILE_PATH = "pr_agent/cli_pip.py"
|
||||||
|
|
||||||
|
PR_HEADER_START_WITH = '### **User description**\nupdate cli_pip.py\n\n\n___\n\n### **PR Type**'
|
||||||
|
REVIEW_START_WITH = '## PR Reviewer Guide 🔍\n\n<table>\n<tr><td>⏱️ <strong>Estimated effort to review</strong>:'
|
||||||
|
IMPROVE_START_WITH_REGEX_PATTERN = r'^## PR Code Suggestions ✨\n\n<!-- [a-z0-9]+ -->\n\n<table><thead><tr><td>Category</td>'
|
||||||
|
|
||||||
|
NUM_MINUTES = 5
|
||||||
|
|
||||||
|
NEW_FILE_CONTENT = """\
|
||||||
|
from pr_agent import cli
|
||||||
|
from pr_agent.config_loader import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Fill in the following values
|
||||||
|
provider = "github" # GitHub provider
|
||||||
|
user_token = "..." # GitHub user token
|
||||||
|
openai_key = "ghs_afsdfasdfsdf" # Example OpenAI key
|
||||||
|
pr_url = "..." # PR URL, for example 'https://github.com/Codium-ai/pr-agent/pull/809'
|
||||||
|
command = "/improve" # Command to run (e.g. '/review', '/describe', 'improve', '/ask="What is the purpose of this PR?"')
|
||||||
|
|
||||||
|
# Setting the configurations
|
||||||
|
get_settings().set("CONFIG.git_provider", provider)
|
||||||
|
get_settings().set("openai.key", openai_key)
|
||||||
|
get_settings().set("github.user_token", user_token)
|
||||||
|
|
||||||
|
# Run the command. Feedback will appear in GitHub PR comments
|
||||||
|
output = cli.run_command(pr_url, command)
|
||||||
|
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
"""
|
||||||
|
|
100
tests/e2e_tests/test_bitbucket_app.py
Normal file
100
tests/e2e_tests/test_bitbucket_app.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from atlassian.bitbucket import Cloud
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.log import setup_logger, get_logger
|
||||||
|
from tests.e2e_tests.e2e_utils import NEW_FILE_CONTENT, FILE_PATH, PR_HEADER_START_WITH, REVIEW_START_WITH, \
|
||||||
|
IMPROVE_START_WITH_REGEX_PATTERN, NUM_MINUTES
|
||||||
|
|
||||||
|
|
||||||
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
|
setup_logger(log_level)
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
def test_e2e_run_bitbucket_app():
|
||||||
|
repo_slug = 'pr-agent-tests'
|
||||||
|
project_key = 'codiumai'
|
||||||
|
base_branch = "main" # or any base branch you want
|
||||||
|
new_branch = f"bitbucket_app_e2e_test-{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
|
||||||
|
get_settings().config.git_provider = "bitbucket"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Add username and password for authentication
|
||||||
|
username = get_settings().get("BITBUCKET.USERNAME", None)
|
||||||
|
password = get_settings().get("BITBUCKET.PASSWORD", None)
|
||||||
|
s = requests.Session()
|
||||||
|
s.auth = (username, password) # Use HTTP Basic Auth
|
||||||
|
bitbucket_client = Cloud(session=s)
|
||||||
|
repo = bitbucket_client.workspaces.get(workspace=project_key).repositories.get(repo_slug)
|
||||||
|
|
||||||
|
# Create a new branch from the base branch
|
||||||
|
logger.info(f"Creating a new branch {new_branch} from {base_branch}")
|
||||||
|
source_branch = repo.branches.get(base_branch)
|
||||||
|
target_repo = repo.branches.create(new_branch,source_branch.hash)
|
||||||
|
|
||||||
|
# Update the file content
|
||||||
|
url = (f"https://api.bitbucket.org/2.0/repositories/{project_key}/{repo_slug}/src")
|
||||||
|
files={FILE_PATH: NEW_FILE_CONTENT}
|
||||||
|
data={
|
||||||
|
"message": "update cli_pip.py",
|
||||||
|
"branch": new_branch,
|
||||||
|
}
|
||||||
|
requests.request("POST", url, auth=HTTPBasicAuth(username, password), data=data, files=files)
|
||||||
|
|
||||||
|
|
||||||
|
# Create a pull request
|
||||||
|
logger.info(f"Creating a pull request from {new_branch} to {base_branch}")
|
||||||
|
pr = repo.pullrequests.create(
|
||||||
|
title=f'{new_branch}',
|
||||||
|
description="update cli_pip.py",
|
||||||
|
source_branch=new_branch,
|
||||||
|
destination_branch=base_branch
|
||||||
|
)
|
||||||
|
|
||||||
|
# check every 1 minute, for 5 minutes if the PR has all the tool results
|
||||||
|
for i in range(NUM_MINUTES):
|
||||||
|
logger.info(f"Waiting for the PR to get all the tool results...")
|
||||||
|
time.sleep(60)
|
||||||
|
comments = list(pr.comments())
|
||||||
|
comments_raw = [c.raw for c in comments]
|
||||||
|
if len(comments) >= 5: # header, 3 suggestions, 1 review
|
||||||
|
valid_review = False
|
||||||
|
for comment_raw in comments_raw:
|
||||||
|
if comment_raw.startswith('## PR Reviewer Guide 🔍'):
|
||||||
|
valid_review = True
|
||||||
|
break
|
||||||
|
if valid_review:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.error(f"REVIEW feedback is invalid")
|
||||||
|
raise Exception("REVIEW feedback is invalid")
|
||||||
|
else:
|
||||||
|
logger.info(f"Waiting for the PR to get all the tool results. {i + 1} minute(s) passed")
|
||||||
|
else:
|
||||||
|
assert False, f"After {NUM_MINUTES} minutes, the PR did not get all the tool results"
|
||||||
|
|
||||||
|
# cleanup - delete the branch
|
||||||
|
pr.decline()
|
||||||
|
repo.branches.delete(new_branch)
|
||||||
|
|
||||||
|
# If we reach here, the test is successful
|
||||||
|
logger.info(f"Succeeded in running e2e test for Bitbucket app on the PR")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to run e2e test for Bitbucket app: {e}")
|
||||||
|
# delete the branch
|
||||||
|
pr.decline()
|
||||||
|
repo.branches.delete(new_branch)
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_e2e_run_bitbucket_app()
|
96
tests/e2e_tests/test_github_app.py
Normal file
96
tests/e2e_tests/test_github_app.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.git_providers import get_git_provider
|
||||||
|
from pr_agent.log import setup_logger, get_logger
|
||||||
|
from tests.e2e_tests.e2e_utils import NEW_FILE_CONTENT, FILE_PATH, PR_HEADER_START_WITH, REVIEW_START_WITH, \
|
||||||
|
IMPROVE_START_WITH_REGEX_PATTERN, NUM_MINUTES
|
||||||
|
|
||||||
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
|
setup_logger(log_level)
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def test_e2e_run_github_app():
|
||||||
|
"""
|
||||||
|
What we want to do:
|
||||||
|
(1) open a PR in a repo 'https://github.com/Codium-ai/pr-agent-tests'
|
||||||
|
(2) wait for 5 minutes until the PR is processed by the GitHub app
|
||||||
|
(3) check that the relevant tools have been executed
|
||||||
|
"""
|
||||||
|
base_branch = "main" # or any base branch you want
|
||||||
|
new_branch = f"github_app_e2e_test-{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
|
||||||
|
repo_url = 'Codium-ai/pr-agent-tests'
|
||||||
|
get_settings().config.git_provider = "github"
|
||||||
|
git_provider = get_git_provider()()
|
||||||
|
github_client = git_provider.github_client
|
||||||
|
repo = github_client.get_repo(repo_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a new branch from the base branch
|
||||||
|
source = repo.get_branch(base_branch)
|
||||||
|
logger.info(f"Creating a new branch {new_branch} from {base_branch}")
|
||||||
|
repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=source.commit.sha)
|
||||||
|
|
||||||
|
# Get the file you want to edit
|
||||||
|
file = repo.get_contents(FILE_PATH, ref=base_branch)
|
||||||
|
# content = file.decoded_content.decode()
|
||||||
|
|
||||||
|
# Update the file content
|
||||||
|
logger.info(f"Updating the file {FILE_PATH}")
|
||||||
|
commit_message = "update cli_pip.py"
|
||||||
|
repo.update_file(
|
||||||
|
file.path,
|
||||||
|
commit_message,
|
||||||
|
NEW_FILE_CONTENT,
|
||||||
|
file.sha,
|
||||||
|
branch=new_branch
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a pull request
|
||||||
|
logger.info(f"Creating a pull request from {new_branch} to {base_branch}")
|
||||||
|
pr = repo.create_pull(
|
||||||
|
title=new_branch,
|
||||||
|
body="update cli_pip.py",
|
||||||
|
head=new_branch,
|
||||||
|
base=base_branch
|
||||||
|
)
|
||||||
|
|
||||||
|
# check every 1 minute, for 5, minutes if the PR has all the tool results
|
||||||
|
for i in range(NUM_MINUTES):
|
||||||
|
logger.info(f"Waiting for the PR to get all the tool results...")
|
||||||
|
time.sleep(60)
|
||||||
|
logger.info(f"Checking the PR {pr.html_url} after {i + 1} minute(s)")
|
||||||
|
pr.update()
|
||||||
|
pr_header_body = pr.body
|
||||||
|
comments = list(pr.get_issue_comments())
|
||||||
|
if len(comments) == 2:
|
||||||
|
comments_body = [comment.body for comment in comments]
|
||||||
|
assert pr_header_body.startswith(PR_HEADER_START_WITH), "DESCRIBE feedback is invalid"
|
||||||
|
assert comments_body[0].startswith(REVIEW_START_WITH), "REVIEW feedback is invalid"
|
||||||
|
assert re.match(IMPROVE_START_WITH_REGEX_PATTERN, comments_body[1]), "IMPROVE feedback is invalid"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info(f"Waiting for the PR to get all the tool results. {i + 1} minute(s) passed")
|
||||||
|
else:
|
||||||
|
assert False, f"After {NUM_MINUTES} minutes, the PR did not get all the tool results"
|
||||||
|
|
||||||
|
# cleanup - delete the branch
|
||||||
|
logger.info(f"Deleting the branch {new_branch}")
|
||||||
|
repo.get_git_ref(f"heads/{new_branch}").delete()
|
||||||
|
|
||||||
|
# If we reach here, the test is successful
|
||||||
|
logger.info(f"Succeeded in running e2e test for GitHub app on the PR {pr.html_url}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to run e2e test for GitHub app: {e}")
|
||||||
|
# delete the branch
|
||||||
|
logger.info(f"Deleting the branch {new_branch}")
|
||||||
|
repo.get_git_ref(f"heads/{new_branch}").delete()
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_e2e_run_github_app()
|
91
tests/e2e_tests/test_gitlab_webhook.py
Normal file
91
tests/e2e_tests/test_gitlab_webhook.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import gitlab
|
||||||
|
|
||||||
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.git_providers import get_git_provider
|
||||||
|
from pr_agent.log import setup_logger, get_logger
|
||||||
|
from tests.e2e_tests.e2e_utils import NEW_FILE_CONTENT, FILE_PATH, PR_HEADER_START_WITH, REVIEW_START_WITH, \
|
||||||
|
IMPROVE_START_WITH_REGEX_PATTERN, NUM_MINUTES
|
||||||
|
|
||||||
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
|
setup_logger(log_level)
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
def test_e2e_run_github_app():
|
||||||
|
# GitLab setup
|
||||||
|
GITLAB_URL = "https://gitlab.com"
|
||||||
|
GITLAB_TOKEN = get_settings().gitlab.PERSONAL_ACCESS_TOKEN
|
||||||
|
gl = gitlab.Gitlab(GITLAB_URL, private_token=GITLAB_TOKEN)
|
||||||
|
repo_url = 'codiumai/pr-agent-tests'
|
||||||
|
project = gl.projects.get(repo_url)
|
||||||
|
|
||||||
|
base_branch = "main" # or any base branch you want
|
||||||
|
new_branch = f"github_app_e2e_test-{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a new branch from the base branch
|
||||||
|
logger.info(f"Creating a new branch {new_branch} from {base_branch}")
|
||||||
|
project.branches.create({'branch': new_branch, 'ref': base_branch})
|
||||||
|
|
||||||
|
# Get the file you want to edit
|
||||||
|
file = project.files.get(file_path=FILE_PATH, ref=base_branch)
|
||||||
|
# content = file.decode()
|
||||||
|
|
||||||
|
# Update the file content
|
||||||
|
logger.info(f"Updating the file {FILE_PATH}")
|
||||||
|
commit_message = "update cli_pip.py"
|
||||||
|
file.content = NEW_FILE_CONTENT
|
||||||
|
file.save(branch=new_branch, commit_message=commit_message)
|
||||||
|
|
||||||
|
# Create a merge request
|
||||||
|
logger.info(f"Creating a merge request from {new_branch} to {base_branch}")
|
||||||
|
mr = project.mergerequests.create({
|
||||||
|
'source_branch': new_branch,
|
||||||
|
'target_branch': base_branch,
|
||||||
|
'title': new_branch,
|
||||||
|
'description': "update cli_pip.py"
|
||||||
|
})
|
||||||
|
logger.info(f"Merge request created: {mr.web_url}")
|
||||||
|
|
||||||
|
# check every 1 minute, for 5, minutes if the PR has all the tool results
|
||||||
|
for i in range(NUM_MINUTES):
|
||||||
|
logger.info(f"Waiting for the MR to get all the tool results...")
|
||||||
|
time.sleep(60)
|
||||||
|
logger.info(f"Checking the MR {mr.web_url} after {i + 1} minute(s)")
|
||||||
|
mr = project.mergerequests.get(mr.iid)
|
||||||
|
mr_header_body = mr.description
|
||||||
|
comments = mr.notes.list()[::-1]
|
||||||
|
# clean all system comments
|
||||||
|
comments = [comment for comment in comments if comment.system is False]
|
||||||
|
if len(comments) == 2: # "changed the description" is received as the first comment
|
||||||
|
comments_body = [comment.body for comment in comments]
|
||||||
|
if 'Work in progress' in comments_body[1]:
|
||||||
|
continue
|
||||||
|
assert mr_header_body.startswith(PR_HEADER_START_WITH), "DESCRIBE feedback is invalid"
|
||||||
|
assert comments_body[0].startswith(REVIEW_START_WITH), "REVIEW feedback is invalid"
|
||||||
|
assert re.match(IMPROVE_START_WITH_REGEX_PATTERN, comments_body[1]), "IMPROVE feedback is invalid"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info(f"Waiting for the MR to get all the tool results. {i + 1} minute(s) passed")
|
||||||
|
else:
|
||||||
|
assert False, f"After {NUM_MINUTES} minutes, the MR did not get all the tool results"
|
||||||
|
|
||||||
|
# cleanup - delete the branch
|
||||||
|
logger.info(f"Deleting the branch {new_branch}")
|
||||||
|
project.branches.delete(new_branch)
|
||||||
|
|
||||||
|
# If we reach here, the test is successful
|
||||||
|
logger.info(f"Succeeded in running e2e test for GitLab app on the MR {mr.web_url}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to run e2e test for GitHub app: {e}")
|
||||||
|
logger.info(f"Deleting the branch {new_branch}")
|
||||||
|
project.branches.delete(new_branch)
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_e2e_run_github_app()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user