xai-review 0.18.0__py3-none-any.whl → 0.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xai-review might be problematic. Click here for more details.
- ai_review/clients/github/pr/client.py +13 -7
- ai_review/clients/github/pr/schema/pull_request.py +6 -6
- ai_review/clients/gitlab/mr/client.py +29 -20
- ai_review/clients/gitlab/mr/schema/changes.py +5 -5
- ai_review/clients/gitlab/mr/schema/discussions.py +1 -4
- ai_review/clients/gitlab/mr/schema/notes.py +19 -0
- ai_review/services/llm/factory.py +1 -1
- ai_review/services/prompt/adapter.py +15 -15
- ai_review/services/prompt/schema.py +18 -18
- ai_review/services/review/service.py +45 -42
- ai_review/services/vcs/factory.py +1 -1
- ai_review/services/vcs/github/client.py +52 -34
- ai_review/services/vcs/gitlab/client.py +62 -44
- ai_review/services/vcs/types.py +38 -29
- ai_review/tests/suites/services/cost/__init__.py +0 -0
- ai_review/tests/suites/services/cost/test_schema.py +124 -0
- ai_review/tests/suites/services/cost/test_service.py +99 -0
- ai_review/tests/suites/services/prompt/test_adapter.py +59 -0
- ai_review/tests/suites/services/prompt/test_schema.py +18 -18
- ai_review/tests/suites/services/prompt/test_service.py +13 -11
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/METADATA +21 -6
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/RECORD +26 -22
- ai_review/clients/gitlab/mr/schema/comments.py +0 -19
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/WHEEL +0 -0
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/top_level.txt +0 -0
|
@@ -11,21 +11,31 @@ from ai_review.clients.github.pr.schema.files import GitHubGetPRFilesResponseSch
|
|
|
11
11
|
from ai_review.clients.github.pr.schema.pull_request import GitHubGetPRResponseSchema
|
|
12
12
|
from ai_review.clients.github.pr.schema.reviews import GitHubGetPRReviewsResponseSchema
|
|
13
13
|
from ai_review.libs.http.client import HTTPClient
|
|
14
|
+
from ai_review.libs.http.handlers import HTTPClientError, handle_http_error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitHubPullRequestsHTTPClientError(HTTPClientError):
|
|
18
|
+
pass
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
class GitHubPullRequestsHTTPClient(HTTPClient):
|
|
22
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
17
23
|
async def get_pull_request_api(self, owner: str, repo: str, pull_number: str) -> Response:
|
|
18
24
|
return await self.get(f"/repos/{owner}/{repo}/pulls/{pull_number}")
|
|
19
25
|
|
|
26
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
20
27
|
async def get_files_api(self, owner: str, repo: str, pull_number: str) -> Response:
|
|
21
28
|
return await self.get(f"/repos/{owner}/{repo}/pulls/{pull_number}/files")
|
|
22
29
|
|
|
30
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
23
31
|
async def get_issue_comments_api(self, owner: str, repo: str, issue_number: str) -> Response:
|
|
24
32
|
return await self.get(f"/repos/{owner}/{repo}/issues/{issue_number}/comments")
|
|
25
33
|
|
|
34
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
26
35
|
async def get_review_comments_api(self, owner: str, repo: str, pull_number: str) -> Response:
|
|
27
36
|
return await self.get(f"/repos/{owner}/{repo}/pulls/{pull_number}/comments")
|
|
28
37
|
|
|
38
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
29
39
|
async def create_review_comment_api(
|
|
30
40
|
self,
|
|
31
41
|
owner: str,
|
|
@@ -38,6 +48,7 @@ class GitHubPullRequestsHTTPClient(HTTPClient):
|
|
|
38
48
|
json=request.model_dump(),
|
|
39
49
|
)
|
|
40
50
|
|
|
51
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
41
52
|
async def create_issue_comment_api(
|
|
42
53
|
self,
|
|
43
54
|
owner: str,
|
|
@@ -50,6 +61,7 @@ class GitHubPullRequestsHTTPClient(HTTPClient):
|
|
|
50
61
|
json=request.model_dump(),
|
|
51
62
|
)
|
|
52
63
|
|
|
64
|
+
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
53
65
|
async def get_reviews_api(self, owner: str, repo: str, pull_number: str) -> Response:
|
|
54
66
|
return await self.get(f"/repos/{owner}/{repo}/pulls/{pull_number}/reviews")
|
|
55
67
|
|
|
@@ -78,14 +90,8 @@ class GitHubPullRequestsHTTPClient(HTTPClient):
|
|
|
78
90
|
owner: str,
|
|
79
91
|
repo: str,
|
|
80
92
|
pull_number: str,
|
|
81
|
-
|
|
82
|
-
commit_id: str,
|
|
83
|
-
path: str,
|
|
84
|
-
line: int,
|
|
93
|
+
request: GitHubCreateReviewCommentRequestSchema
|
|
85
94
|
) -> GitHubCreateReviewCommentResponseSchema:
|
|
86
|
-
request = GitHubCreateReviewCommentRequestSchema(
|
|
87
|
-
body=body, commit_id=commit_id, path=path, line=line
|
|
88
|
-
)
|
|
89
95
|
response = await self.create_review_comment_api(owner, repo, pull_number, request)
|
|
90
96
|
return GitHubCreateReviewCommentResponseSchema.model_validate_json(response.text)
|
|
91
97
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class GitHubUserSchema(BaseModel):
|
|
@@ -8,13 +8,13 @@ class GitHubUserSchema(BaseModel):
|
|
|
8
8
|
|
|
9
9
|
class GitHubLabelSchema(BaseModel):
|
|
10
10
|
id: int
|
|
11
|
-
name: str
|
|
11
|
+
name: str | None = None
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class GitHubBranchSchema(BaseModel):
|
|
15
15
|
ref: str
|
|
16
16
|
sha: str
|
|
17
|
-
label: str
|
|
17
|
+
label: str | None = None
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class GitHubGetPRResponseSchema(BaseModel):
|
|
@@ -23,8 +23,8 @@ class GitHubGetPRResponseSchema(BaseModel):
|
|
|
23
23
|
title: str
|
|
24
24
|
body: str | None = None
|
|
25
25
|
user: GitHubUserSchema
|
|
26
|
-
labels: list[GitHubLabelSchema]
|
|
27
|
-
assignees: list[GitHubUserSchema] =
|
|
28
|
-
requested_reviewers: list[GitHubUserSchema] =
|
|
26
|
+
labels: list[GitHubLabelSchema] = Field(default_factory=list)
|
|
27
|
+
assignees: list[GitHubUserSchema] = Field(default_factory=list)
|
|
28
|
+
requested_reviewers: list[GitHubUserSchema] = Field(default_factory=list)
|
|
29
29
|
base: GitHubBranchSchema
|
|
30
30
|
head: GitHubBranchSchema
|
|
@@ -1,47 +1,56 @@
|
|
|
1
1
|
from httpx import Response
|
|
2
2
|
|
|
3
3
|
from ai_review.clients.gitlab.mr.schema.changes import GitLabGetMRChangesResponseSchema
|
|
4
|
-
from ai_review.clients.gitlab.mr.schema.comments import (
|
|
5
|
-
GitLabGetMRCommentsResponseSchema,
|
|
6
|
-
GitLabCreateMRCommentRequestSchema,
|
|
7
|
-
GitLabCreateMRCommentResponseSchema,
|
|
8
|
-
)
|
|
9
4
|
from ai_review.clients.gitlab.mr.schema.discussions import (
|
|
10
5
|
GitLabGetMRDiscussionsResponseSchema,
|
|
11
6
|
GitLabCreateMRDiscussionRequestSchema,
|
|
12
7
|
GitLabCreateMRDiscussionResponseSchema
|
|
13
8
|
)
|
|
9
|
+
from ai_review.clients.gitlab.mr.schema.notes import (
|
|
10
|
+
GitLabGetMRNotesResponseSchema,
|
|
11
|
+
GitLabCreateMRNoteRequestSchema,
|
|
12
|
+
GitLabCreateMRNoteResponseSchema,
|
|
13
|
+
)
|
|
14
14
|
from ai_review.libs.http.client import HTTPClient
|
|
15
|
+
from ai_review.libs.http.handlers import handle_http_error, HTTPClientError
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class
|
|
18
|
+
class GitLabMergeRequestsHTTPClientError(HTTPClientError):
|
|
19
|
+
pass
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
23
|
+
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
19
24
|
async def get_changes_api(self, project_id: str, merge_request_id: str) -> Response:
|
|
20
25
|
return await self.get(
|
|
21
26
|
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/changes"
|
|
22
27
|
)
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
30
|
+
async def get_notes_api(self, project_id: str, merge_request_id: str) -> Response:
|
|
25
31
|
return await self.get(
|
|
26
32
|
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/notes"
|
|
27
33
|
)
|
|
28
34
|
|
|
35
|
+
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
29
36
|
async def get_discussions_api(self, project_id: str, merge_request_id: str) -> Response:
|
|
30
37
|
return await self.get(
|
|
31
38
|
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/discussions"
|
|
32
39
|
)
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
42
|
+
async def create_note_api(
|
|
35
43
|
self,
|
|
36
44
|
project_id: str,
|
|
37
45
|
merge_request_id: str,
|
|
38
|
-
request:
|
|
46
|
+
request: GitLabCreateMRNoteRequestSchema,
|
|
39
47
|
) -> Response:
|
|
40
48
|
return await self.post(
|
|
41
49
|
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/notes",
|
|
42
50
|
json=request.model_dump(),
|
|
43
51
|
)
|
|
44
52
|
|
|
53
|
+
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
45
54
|
async def create_discussion_api(
|
|
46
55
|
self,
|
|
47
56
|
project_id: str,
|
|
@@ -57,13 +66,13 @@ class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
|
57
66
|
response = await self.get_changes_api(project_id, merge_request_id)
|
|
58
67
|
return GitLabGetMRChangesResponseSchema.model_validate_json(response.text)
|
|
59
68
|
|
|
60
|
-
async def
|
|
69
|
+
async def get_notes(
|
|
61
70
|
self,
|
|
62
71
|
project_id: str,
|
|
63
72
|
merge_request_id: str
|
|
64
|
-
) ->
|
|
65
|
-
response = await self.
|
|
66
|
-
return
|
|
73
|
+
) -> GitLabGetMRNotesResponseSchema:
|
|
74
|
+
response = await self.get_notes_api(project_id, merge_request_id)
|
|
75
|
+
return GitLabGetMRNotesResponseSchema.model_validate_json(response.text)
|
|
67
76
|
|
|
68
77
|
async def get_discussions(
|
|
69
78
|
self,
|
|
@@ -73,26 +82,26 @@ class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
|
73
82
|
response = await self.get_discussions_api(project_id, merge_request_id)
|
|
74
83
|
return GitLabGetMRDiscussionsResponseSchema.model_validate_json(response.text)
|
|
75
84
|
|
|
76
|
-
async def
|
|
85
|
+
async def create_note(
|
|
77
86
|
self,
|
|
78
|
-
|
|
87
|
+
body: str,
|
|
79
88
|
project_id: str,
|
|
80
89
|
merge_request_id: str,
|
|
81
|
-
) ->
|
|
82
|
-
request =
|
|
83
|
-
response = await self.
|
|
90
|
+
) -> GitLabCreateMRNoteResponseSchema:
|
|
91
|
+
request = GitLabCreateMRNoteRequestSchema(body=body)
|
|
92
|
+
response = await self.create_note_api(
|
|
84
93
|
request=request,
|
|
85
94
|
project_id=project_id,
|
|
86
95
|
merge_request_id=merge_request_id
|
|
87
96
|
)
|
|
88
|
-
return
|
|
97
|
+
return GitLabCreateMRNoteResponseSchema.model_validate_json(response.text)
|
|
89
98
|
|
|
90
99
|
async def create_discussion(
|
|
91
100
|
self,
|
|
92
101
|
project_id: str,
|
|
93
102
|
merge_request_id: str,
|
|
94
103
|
request: GitLabCreateMRDiscussionRequestSchema
|
|
95
|
-
):
|
|
104
|
+
) -> GitLabCreateMRDiscussionResponseSchema:
|
|
96
105
|
response = await self.create_discussion_api(
|
|
97
106
|
request=request,
|
|
98
107
|
project_id=project_id,
|
|
@@ -14,9 +14,9 @@ class GitLabDiffRefsSchema(BaseModel):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class GitLabMRChangeSchema(BaseModel):
|
|
17
|
-
diff: str
|
|
18
|
-
old_path: str
|
|
19
|
-
new_path: str
|
|
17
|
+
diff: str | None = None
|
|
18
|
+
old_path: str | None = None
|
|
19
|
+
new_path: str | None = None
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class GitLabGetMRChangesResponseSchema(BaseModel):
|
|
@@ -24,12 +24,12 @@ class GitLabGetMRChangesResponseSchema(BaseModel):
|
|
|
24
24
|
iid: int
|
|
25
25
|
title: str
|
|
26
26
|
author: GitLabUserSchema
|
|
27
|
-
labels: list[str] =
|
|
27
|
+
labels: list[str] = Field(default_factory=list)
|
|
28
28
|
changes: list[GitLabMRChangeSchema]
|
|
29
29
|
assignees: list[GitLabUserSchema] = Field(default_factory=list)
|
|
30
30
|
reviewers: list[GitLabUserSchema] = Field(default_factory=list)
|
|
31
31
|
diff_refs: GitLabDiffRefsSchema
|
|
32
32
|
project_id: int
|
|
33
|
-
description: str
|
|
33
|
+
description: str | None = None
|
|
34
34
|
source_branch: str
|
|
35
35
|
target_branch: str
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pydantic import BaseModel, RootModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GitLabNoteSchema(BaseModel):
|
|
5
|
+
id: int
|
|
6
|
+
body: str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitLabGetMRNotesResponseSchema(RootModel[list[GitLabNoteSchema]]):
|
|
10
|
+
root: list[GitLabNoteSchema]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GitLabCreateMRNoteRequestSchema(BaseModel):
|
|
14
|
+
body: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitLabCreateMRNoteResponseSchema(BaseModel):
|
|
18
|
+
id: int
|
|
19
|
+
body: str
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
from ai_review.services.prompt.schema import PromptContextSchema
|
|
2
|
-
from ai_review.services.vcs.types import
|
|
2
|
+
from ai_review.services.vcs.types import ReviewInfoSchema
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def build_prompt_context_from_mr_info(
|
|
5
|
+
def build_prompt_context_from_mr_info(review: ReviewInfoSchema) -> PromptContextSchema:
|
|
6
6
|
return PromptContextSchema(
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
review_title=review.title,
|
|
8
|
+
review_description=review.description,
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
review_author_name=review.author.name,
|
|
11
|
+
review_author_username=review.author.username,
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
review_reviewers=[user.name for user in review.reviewers],
|
|
14
|
+
review_reviewers_usernames=[user.username for user in review.reviewers],
|
|
15
|
+
review_reviewer=review.reviewers[0].name if review.reviewers else "",
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
review_assignees=[user.name for user in review.assignees],
|
|
18
|
+
review_assignees_usernames=[user.username for user in review.assignees],
|
|
19
19
|
|
|
20
|
-
source_branch=
|
|
21
|
-
target_branch=
|
|
20
|
+
source_branch=review.source_branch.ref,
|
|
21
|
+
target_branch=review.target_branch.ref,
|
|
22
22
|
|
|
23
|
-
labels=
|
|
24
|
-
changed_files=
|
|
23
|
+
labels=review.labels,
|
|
24
|
+
changed_files=review.changed_files,
|
|
25
25
|
)
|
|
@@ -5,18 +5,18 @@ from ai_review.libs.template.render import render_template
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class PromptContextSchema(BaseModel):
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
review_title: str = ""
|
|
9
|
+
review_description: str = ""
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
review_author_name: str = ""
|
|
12
|
+
review_author_username: str = ""
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
review_reviewer: str = ""
|
|
15
|
+
review_reviewers: list[str] = Field(default_factory=list)
|
|
16
|
+
review_reviewers_usernames: list[str] = Field(default_factory=list)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
review_assignees: list[str] = Field(default_factory=list)
|
|
19
|
+
review_assignees_usernames: list[str] = Field(default_factory=list)
|
|
20
20
|
|
|
21
21
|
source_branch: str = ""
|
|
22
22
|
target_branch: str = ""
|
|
@@ -27,18 +27,18 @@ class PromptContextSchema(BaseModel):
|
|
|
27
27
|
@property
|
|
28
28
|
def render_values(self) -> dict[str, str]:
|
|
29
29
|
return {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"review_title": self.review_title,
|
|
31
|
+
"review_description": self.review_description,
|
|
32
32
|
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"review_author_name": self.review_author_name,
|
|
34
|
+
"review_author_username": self.review_author_username,
|
|
35
35
|
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"review_reviewer": self.review_reviewer,
|
|
37
|
+
"review_reviewers": ", ".join(self.review_reviewers),
|
|
38
|
+
"review_reviewers_usernames": ", ".join(self.review_reviewers_usernames),
|
|
39
39
|
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"review_assignees": ", ".join(self.review_assignees),
|
|
41
|
+
"review_assignees_usernames": ", ".join(self.review_assignees_usernames),
|
|
42
42
|
|
|
43
43
|
"source_branch": self.source_branch,
|
|
44
44
|
"target_branch": self.target_branch,
|
|
@@ -15,7 +15,7 @@ from ai_review.services.review.inline.service import InlineCommentService
|
|
|
15
15
|
from ai_review.services.review.policy.service import ReviewPolicyService
|
|
16
16
|
from ai_review.services.review.summary.service import SummaryCommentService
|
|
17
17
|
from ai_review.services.vcs.factory import get_vcs_client
|
|
18
|
-
from ai_review.services.vcs.types import
|
|
18
|
+
from ai_review.services.vcs.types import ReviewInfoSchema
|
|
19
19
|
|
|
20
20
|
logger = get_logger("REVIEW_SERVICE")
|
|
21
21
|
|
|
@@ -52,37 +52,43 @@ class ReviewService:
|
|
|
52
52
|
logger.exception(f"LLM request failed: {error}")
|
|
53
53
|
raise
|
|
54
54
|
|
|
55
|
-
async def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
settings.review.inline_tag in
|
|
59
|
-
for
|
|
60
|
-
for note in discussion.notes
|
|
55
|
+
async def has_existing_inline_comments(self) -> bool:
|
|
56
|
+
comments = await self.vcs.get_inline_comments()
|
|
57
|
+
has_comments = any(
|
|
58
|
+
settings.review.inline_tag in comment.body
|
|
59
|
+
for comment in comments
|
|
61
60
|
)
|
|
62
|
-
if
|
|
63
|
-
logger.info("Skipping inline review: AI inline
|
|
61
|
+
if has_comments:
|
|
62
|
+
logger.info("Skipping inline review: AI inline comments already exist")
|
|
64
63
|
|
|
65
|
-
return
|
|
64
|
+
return has_comments
|
|
66
65
|
|
|
67
66
|
async def has_existing_summary_comments(self) -> bool:
|
|
68
|
-
comments = await self.vcs.
|
|
69
|
-
has_comments = any(
|
|
67
|
+
comments = await self.vcs.get_general_comments()
|
|
68
|
+
has_comments = any(
|
|
69
|
+
settings.review.summary_tag in comment.body for comment in comments
|
|
70
|
+
)
|
|
70
71
|
if has_comments:
|
|
71
72
|
logger.info("Skipping summary review: AI summary comment already exists")
|
|
72
73
|
|
|
73
74
|
return has_comments
|
|
74
75
|
|
|
75
|
-
async def
|
|
76
|
+
async def process_inline_comments(
|
|
77
|
+
self,
|
|
78
|
+
flow: Literal["inline", "context"],
|
|
79
|
+
comments: InlineCommentListSchema
|
|
80
|
+
) -> None:
|
|
76
81
|
results = await bounded_gather([
|
|
77
|
-
self.vcs.
|
|
82
|
+
self.vcs.create_inline_comment(
|
|
78
83
|
file=comment.file,
|
|
79
84
|
line=comment.line,
|
|
80
85
|
message=comment.body_with_tag
|
|
81
86
|
)
|
|
82
87
|
for comment in comments.root
|
|
83
88
|
])
|
|
89
|
+
|
|
84
90
|
fallbacks = [
|
|
85
|
-
self.vcs.
|
|
91
|
+
self.vcs.create_general_comment(comment.fallback_body_with_tag)
|
|
86
92
|
for comment, result in zip(comments.root, results)
|
|
87
93
|
if isinstance(result, Exception)
|
|
88
94
|
]
|
|
@@ -90,19 +96,19 @@ class ReviewService:
|
|
|
90
96
|
logger.warning(f"Falling back to {len(fallbacks)} general comments ({flow} review)")
|
|
91
97
|
await bounded_gather(fallbacks)
|
|
92
98
|
|
|
93
|
-
async def process_file_inline(self, file: str,
|
|
94
|
-
raw_diff = self.git.get_diff_for_file(
|
|
99
|
+
async def process_file_inline(self, file: str, review_info: ReviewInfoSchema) -> None:
|
|
100
|
+
raw_diff = self.git.get_diff_for_file(review_info.base_sha, review_info.head_sha, file)
|
|
95
101
|
if not raw_diff.strip():
|
|
96
102
|
logger.debug(f"No diff for {file}, skipping")
|
|
97
103
|
return
|
|
98
104
|
|
|
99
105
|
rendered_file = self.diff.render_file(
|
|
100
106
|
file=file,
|
|
101
|
-
base_sha=
|
|
102
|
-
head_sha=
|
|
107
|
+
base_sha=review_info.base_sha,
|
|
108
|
+
head_sha=review_info.head_sha,
|
|
103
109
|
raw_diff=raw_diff,
|
|
104
110
|
)
|
|
105
|
-
prompt_context = build_prompt_context_from_mr_info(
|
|
111
|
+
prompt_context = build_prompt_context_from_mr_info(review_info)
|
|
106
112
|
prompt = self.prompt.build_inline_request(rendered_file, prompt_context)
|
|
107
113
|
prompt_system = self.prompt.build_system_inline_request(prompt_context)
|
|
108
114
|
prompt_result = await self.ask_llm(prompt, prompt_system)
|
|
@@ -114,29 +120,27 @@ class ReviewService:
|
|
|
114
120
|
return
|
|
115
121
|
|
|
116
122
|
logger.info(f"Posting {len(comments.root)} inline comments to {file}")
|
|
117
|
-
await self.
|
|
123
|
+
await self.process_inline_comments(flow="inline", comments=comments)
|
|
118
124
|
|
|
119
125
|
async def run_inline_review(self) -> None:
|
|
120
|
-
if await self.
|
|
126
|
+
if await self.has_existing_inline_comments():
|
|
121
127
|
return
|
|
122
128
|
|
|
123
|
-
|
|
129
|
+
review_info = await self.vcs.get_review_info()
|
|
130
|
+
logger.info(f"Starting inline review: {len(review_info.changed_files)} files changed")
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
changed_files = self.policy.apply_for_files(mr_info.changed_files)
|
|
132
|
+
changed_files = self.policy.apply_for_files(review_info.changed_files)
|
|
128
133
|
await bounded_gather([
|
|
129
|
-
self.process_file_inline(changed_file,
|
|
134
|
+
self.process_file_inline(changed_file, review_info)
|
|
130
135
|
for changed_file in changed_files
|
|
131
136
|
])
|
|
132
137
|
|
|
133
138
|
async def run_context_review(self) -> None:
|
|
134
|
-
if await self.
|
|
139
|
+
if await self.has_existing_inline_comments():
|
|
135
140
|
return
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
changed_files = self.policy.apply_for_files(
|
|
139
|
-
|
|
142
|
+
review_info = await self.vcs.get_review_info()
|
|
143
|
+
changed_files = self.policy.apply_for_files(review_info.changed_files)
|
|
140
144
|
if not changed_files:
|
|
141
145
|
logger.info("No files to review for context review")
|
|
142
146
|
return
|
|
@@ -146,10 +150,10 @@ class ReviewService:
|
|
|
146
150
|
rendered_files = self.diff.render_files(
|
|
147
151
|
git=self.git,
|
|
148
152
|
files=changed_files,
|
|
149
|
-
base_sha=
|
|
150
|
-
head_sha=
|
|
153
|
+
base_sha=review_info.base_sha,
|
|
154
|
+
head_sha=review_info.head_sha,
|
|
151
155
|
)
|
|
152
|
-
prompt_context = build_prompt_context_from_mr_info(
|
|
156
|
+
prompt_context = build_prompt_context_from_mr_info(review_info)
|
|
153
157
|
prompt = self.prompt.build_context_request(rendered_files, prompt_context)
|
|
154
158
|
prompt_system = self.prompt.build_system_context_request(prompt_context)
|
|
155
159
|
prompt_result = await self.ask_llm(prompt, prompt_system)
|
|
@@ -161,15 +165,14 @@ class ReviewService:
|
|
|
161
165
|
return
|
|
162
166
|
|
|
163
167
|
logger.info(f"Posting {len(comments.root)} inline comments (context review)")
|
|
164
|
-
await self.
|
|
168
|
+
await self.process_inline_comments(flow="context", comments=comments)
|
|
165
169
|
|
|
166
170
|
async def run_summary_review(self) -> None:
|
|
167
171
|
if await self.has_existing_summary_comments():
|
|
168
172
|
return
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
changed_files = self.policy.apply_for_files(
|
|
172
|
-
|
|
174
|
+
review_info = await self.vcs.get_review_info()
|
|
175
|
+
changed_files = self.policy.apply_for_files(review_info.changed_files)
|
|
173
176
|
if not changed_files:
|
|
174
177
|
logger.info("No files to review for summary")
|
|
175
178
|
return
|
|
@@ -179,10 +182,10 @@ class ReviewService:
|
|
|
179
182
|
rendered_files = self.diff.render_files(
|
|
180
183
|
git=self.git,
|
|
181
184
|
files=changed_files,
|
|
182
|
-
base_sha=
|
|
183
|
-
head_sha=
|
|
185
|
+
base_sha=review_info.base_sha,
|
|
186
|
+
head_sha=review_info.head_sha,
|
|
184
187
|
)
|
|
185
|
-
prompt_context = build_prompt_context_from_mr_info(
|
|
188
|
+
prompt_context = build_prompt_context_from_mr_info(review_info)
|
|
186
189
|
prompt = self.prompt.build_summary_request(rendered_files, prompt_context)
|
|
187
190
|
prompt_system = self.prompt.build_system_summary_request(prompt_context)
|
|
188
191
|
prompt_result = await self.ask_llm(prompt, prompt_system)
|
|
@@ -193,7 +196,7 @@ class ReviewService:
|
|
|
193
196
|
return
|
|
194
197
|
|
|
195
198
|
logger.info(f"Posting summary review comment ({len(summary.text)} chars)")
|
|
196
|
-
await self.vcs.
|
|
199
|
+
await self.vcs.create_general_comment(summary.body_with_tag)
|
|
197
200
|
|
|
198
201
|
def report_total_cost(self):
|
|
199
202
|
total_report = self.cost.aggregate()
|