xai-review 0.27.0__py3-none-any.whl → 0.29.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/cli/commands/run_inline_reply_review.py +7 -0
- ai_review/cli/commands/run_summary_reply_review.py +7 -0
- ai_review/cli/main.py +17 -0
- ai_review/clients/bitbucket/pr/schema/comments.py +14 -0
- ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
- ai_review/clients/bitbucket/pr/schema/user.py +7 -0
- ai_review/clients/github/pr/client.py +35 -4
- ai_review/clients/github/pr/schema/comments.py +21 -0
- ai_review/clients/github/pr/schema/pull_request.py +1 -4
- ai_review/clients/github/pr/schema/user.py +6 -0
- ai_review/clients/github/pr/types.py +11 -1
- ai_review/clients/gitlab/mr/client.py +32 -1
- ai_review/clients/gitlab/mr/schema/changes.py +1 -5
- ai_review/clients/gitlab/mr/schema/discussions.py +14 -12
- ai_review/clients/gitlab/mr/schema/notes.py +5 -0
- ai_review/clients/gitlab/mr/schema/position.py +13 -0
- ai_review/clients/gitlab/mr/schema/user.py +7 -0
- ai_review/clients/gitlab/mr/types.py +16 -7
- ai_review/libs/asynchronous/gather.py +8 -1
- ai_review/libs/config/prompt.py +96 -64
- ai_review/libs/config/review.py +2 -0
- ai_review/libs/llm/output_json_parser.py +60 -0
- ai_review/prompts/default_inline_reply.md +10 -0
- ai_review/prompts/default_summary_reply.md +14 -0
- ai_review/prompts/default_system_inline_reply.md +31 -0
- ai_review/prompts/default_system_summary_reply.md +13 -0
- ai_review/services/artifacts/schema.py +2 -2
- ai_review/services/git/service.py +42 -11
- ai_review/services/hook/constants.py +14 -0
- ai_review/services/hook/service.py +95 -4
- ai_review/services/hook/types.py +18 -2
- ai_review/services/prompt/adapter.py +1 -1
- ai_review/services/prompt/service.py +49 -3
- ai_review/services/prompt/tools.py +21 -0
- ai_review/services/prompt/types.py +23 -0
- ai_review/services/review/gateway/comment.py +45 -6
- ai_review/services/review/gateway/llm.py +2 -1
- ai_review/services/review/gateway/types.py +50 -0
- ai_review/services/review/internal/inline/service.py +40 -0
- ai_review/services/review/internal/inline/types.py +8 -0
- ai_review/services/review/internal/inline_reply/schema.py +23 -0
- ai_review/services/review/internal/inline_reply/service.py +20 -0
- ai_review/services/review/internal/inline_reply/types.py +8 -0
- ai_review/services/review/{policy → internal/policy}/service.py +2 -1
- ai_review/services/review/internal/policy/types.py +15 -0
- ai_review/services/review/{summary → internal/summary}/service.py +2 -2
- ai_review/services/review/{summary → internal/summary}/types.py +1 -1
- ai_review/services/review/internal/summary_reply/__init__.py +0 -0
- ai_review/services/review/internal/summary_reply/schema.py +8 -0
- ai_review/services/review/internal/summary_reply/service.py +15 -0
- ai_review/services/review/internal/summary_reply/types.py +8 -0
- ai_review/services/review/runner/__init__.py +0 -0
- ai_review/services/review/runner/context.py +72 -0
- ai_review/services/review/runner/inline.py +80 -0
- ai_review/services/review/runner/inline_reply.py +80 -0
- ai_review/services/review/runner/summary.py +71 -0
- ai_review/services/review/runner/summary_reply.py +79 -0
- ai_review/services/review/runner/types.py +6 -0
- ai_review/services/review/service.py +78 -110
- ai_review/services/vcs/bitbucket/adapter.py +27 -0
- ai_review/services/vcs/bitbucket/client.py +118 -42
- ai_review/services/vcs/github/adapter.py +35 -0
- ai_review/services/vcs/github/client.py +105 -44
- ai_review/services/vcs/gitlab/adapter.py +28 -0
- ai_review/services/vcs/gitlab/client.py +103 -43
- ai_review/services/vcs/types.py +34 -0
- ai_review/tests/fixtures/clients/bitbucket.py +2 -2
- ai_review/tests/fixtures/clients/github.py +35 -6
- ai_review/tests/fixtures/clients/gitlab.py +71 -6
- ai_review/tests/fixtures/libs/__init__.py +0 -0
- ai_review/tests/fixtures/libs/llm/__init__.py +0 -0
- ai_review/tests/fixtures/libs/llm/output_json_parser.py +13 -0
- ai_review/tests/fixtures/services/hook.py +8 -0
- ai_review/tests/fixtures/services/llm.py +8 -5
- ai_review/tests/fixtures/services/prompt.py +70 -0
- ai_review/tests/fixtures/services/review/base.py +41 -0
- ai_review/tests/fixtures/services/review/gateway/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/gateway/comment.py +98 -0
- ai_review/tests/fixtures/services/review/gateway/llm.py +17 -0
- ai_review/tests/fixtures/services/review/internal/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/{inline.py → internal/inline.py} +8 -6
- ai_review/tests/fixtures/services/review/internal/inline_reply.py +25 -0
- ai_review/tests/fixtures/services/review/internal/policy.py +28 -0
- ai_review/tests/fixtures/services/review/internal/summary.py +21 -0
- ai_review/tests/fixtures/services/review/internal/summary_reply.py +19 -0
- ai_review/tests/fixtures/services/review/runner/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/runner/context.py +50 -0
- ai_review/tests/fixtures/services/review/runner/inline.py +50 -0
- ai_review/tests/fixtures/services/review/runner/inline_reply.py +50 -0
- ai_review/tests/fixtures/services/review/runner/summary.py +50 -0
- ai_review/tests/fixtures/services/review/runner/summary_reply.py +50 -0
- ai_review/tests/fixtures/services/vcs.py +23 -0
- ai_review/tests/suites/cli/__init__.py +0 -0
- ai_review/tests/suites/cli/test_main.py +54 -0
- ai_review/tests/suites/libs/config/test_prompt.py +108 -28
- ai_review/tests/suites/libs/llm/__init__.py +0 -0
- ai_review/tests/suites/libs/llm/test_output_json_parser.py +155 -0
- ai_review/tests/suites/services/hook/test_service.py +88 -4
- ai_review/tests/suites/services/prompt/test_adapter.py +3 -3
- ai_review/tests/suites/services/prompt/test_service.py +102 -58
- ai_review/tests/suites/services/prompt/test_tools.py +86 -1
- ai_review/tests/suites/services/review/gateway/__init__.py +0 -0
- ai_review/tests/suites/services/review/gateway/test_comment.py +253 -0
- ai_review/tests/suites/services/review/gateway/test_llm.py +82 -0
- ai_review/tests/suites/services/review/internal/__init__.py +0 -0
- ai_review/tests/suites/services/review/internal/inline/__init__.py +0 -0
- ai_review/tests/suites/services/review/{inline → internal/inline}/test_schema.py +1 -1
- ai_review/tests/suites/services/review/internal/inline/test_service.py +81 -0
- ai_review/tests/suites/services/review/internal/inline_reply/__init__.py +0 -0
- ai_review/tests/suites/services/review/internal/inline_reply/test_schema.py +57 -0
- ai_review/tests/suites/services/review/internal/inline_reply/test_service.py +72 -0
- ai_review/tests/suites/services/review/internal/policy/__init__.py +0 -0
- ai_review/tests/suites/services/review/{policy → internal/policy}/test_service.py +1 -1
- ai_review/tests/suites/services/review/internal/summary/__init__.py +0 -0
- ai_review/tests/suites/services/review/{summary → internal/summary}/test_schema.py +1 -1
- ai_review/tests/suites/services/review/{summary → internal/summary}/test_service.py +2 -2
- ai_review/tests/suites/services/review/internal/summary_reply/__init__.py +0 -0
- ai_review/tests/suites/services/review/internal/summary_reply/test_schema.py +19 -0
- ai_review/tests/suites/services/review/internal/summary_reply/test_service.py +21 -0
- ai_review/tests/suites/services/review/runner/__init__.py +0 -0
- ai_review/tests/suites/services/review/runner/test_context.py +89 -0
- ai_review/tests/suites/services/review/runner/test_inline.py +100 -0
- ai_review/tests/suites/services/review/runner/test_inline_reply.py +109 -0
- ai_review/tests/suites/services/review/runner/test_summary.py +87 -0
- ai_review/tests/suites/services/review/runner/test_summary_reply.py +97 -0
- ai_review/tests/suites/services/review/test_service.py +64 -97
- ai_review/tests/suites/services/vcs/bitbucket/test_adapter.py +109 -0
- ai_review/tests/suites/services/vcs/bitbucket/{test_service.py → test_client.py} +88 -1
- ai_review/tests/suites/services/vcs/github/test_adapter.py +162 -0
- ai_review/tests/suites/services/vcs/github/{test_service.py → test_client.py} +102 -2
- ai_review/tests/suites/services/vcs/gitlab/test_adapter.py +134 -0
- ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +113 -3
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/METADATA +8 -5
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/RECORD +146 -72
- ai_review/services/review/inline/service.py +0 -54
- ai_review/services/review/inline/types.py +0 -11
- ai_review/tests/fixtures/services/review/summary.py +0 -19
- ai_review/tests/suites/services/review/inline/test_service.py +0 -107
- /ai_review/{services/review/inline → libs/llm}/__init__.py +0 -0
- /ai_review/services/review/{policy → internal}/__init__.py +0 -0
- /ai_review/services/review/{summary → internal/inline}/__init__.py +0 -0
- /ai_review/services/review/{inline → internal/inline}/schema.py +0 -0
- /ai_review/{tests/suites/services/review/inline → services/review/internal/inline_reply}/__init__.py +0 -0
- /ai_review/{tests/suites/services/review → services/review/internal}/policy/__init__.py +0 -0
- /ai_review/{tests/suites/services/review → services/review/internal}/summary/__init__.py +0 -0
- /ai_review/services/review/{summary → internal/summary}/schema.py +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/WHEEL +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
1
3
|
from ai_review.clients.bitbucket.client import get_bitbucket_http_client
|
|
2
4
|
from ai_review.clients.bitbucket.pr.schema.comments import (
|
|
3
5
|
BitbucketCommentInlineSchema,
|
|
4
6
|
BitbucketCommentContentSchema,
|
|
5
|
-
BitbucketCreatePRCommentRequestSchema,
|
|
7
|
+
BitbucketCreatePRCommentRequestSchema, BitbucketParentSchema,
|
|
6
8
|
)
|
|
7
9
|
from ai_review.config import settings
|
|
8
10
|
from ai_review.libs.logger import get_logger
|
|
11
|
+
from ai_review.services.vcs.bitbucket.adapter import get_review_comment_from_bitbucket_pr_comment
|
|
9
12
|
from ai_review.services.vcs.types import (
|
|
10
13
|
VCSClientProtocol,
|
|
11
14
|
UserSchema,
|
|
12
15
|
BranchRefSchema,
|
|
13
16
|
ReviewInfoSchema,
|
|
14
|
-
ReviewCommentSchema,
|
|
17
|
+
ReviewCommentSchema, ReviewThreadSchema, ThreadKind,
|
|
15
18
|
)
|
|
16
19
|
|
|
17
20
|
logger = get_logger("BITBUCKET_VCS_CLIENT")
|
|
@@ -23,7 +26,9 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
23
26
|
self.workspace = settings.vcs.pipeline.workspace
|
|
24
27
|
self.repo_slug = settings.vcs.pipeline.repo_slug
|
|
25
28
|
self.pull_request_id = settings.vcs.pipeline.pull_request_id
|
|
29
|
+
self.pull_request_ref = f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}"
|
|
26
30
|
|
|
31
|
+
# --- Review info ---
|
|
27
32
|
async def get_review_info(self) -> ReviewInfoSchema:
|
|
28
33
|
try:
|
|
29
34
|
pr = await self.http_client.pr.get_pull_request(
|
|
@@ -37,7 +42,7 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
37
42
|
pull_request_id=self.pull_request_id,
|
|
38
43
|
)
|
|
39
44
|
|
|
40
|
-
logger.info(f"Fetched PR info for {self.
|
|
45
|
+
logger.info(f"Fetched PR info for {self.pull_request_ref}")
|
|
41
46
|
|
|
42
47
|
return ReviewInfoSchema(
|
|
43
48
|
id=pr.id,
|
|
@@ -81,11 +86,10 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
81
86
|
],
|
|
82
87
|
)
|
|
83
88
|
except Exception as error:
|
|
84
|
-
logger.exception(
|
|
85
|
-
f"Failed to fetch PR info {self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
86
|
-
)
|
|
89
|
+
logger.exception(f"Failed to fetch PR info {self.pull_request_ref}: {error}")
|
|
87
90
|
return ReviewInfoSchema()
|
|
88
91
|
|
|
92
|
+
# --- Comments ---
|
|
89
93
|
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
90
94
|
try:
|
|
91
95
|
response = await self.http_client.pr.get_comments(
|
|
@@ -93,18 +97,15 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
93
97
|
repo_slug=self.repo_slug,
|
|
94
98
|
pull_request_id=self.pull_request_id,
|
|
95
99
|
)
|
|
96
|
-
logger.info(f"Fetched general comments for {self.
|
|
100
|
+
logger.info(f"Fetched general comments for {self.pull_request_ref}")
|
|
97
101
|
|
|
98
102
|
return [
|
|
99
|
-
|
|
103
|
+
get_review_comment_from_bitbucket_pr_comment(comment)
|
|
100
104
|
for comment in response.values
|
|
101
105
|
if comment.inline is None
|
|
102
106
|
]
|
|
103
107
|
except Exception as error:
|
|
104
|
-
logger.exception(
|
|
105
|
-
f"Failed to fetch general comments for "
|
|
106
|
-
f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
107
|
-
)
|
|
108
|
+
logger.exception(f"Failed to fetch general comments for {self.pull_request_ref}: {error}")
|
|
108
109
|
return []
|
|
109
110
|
|
|
110
111
|
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
@@ -114,30 +115,20 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
114
115
|
repo_slug=self.repo_slug,
|
|
115
116
|
pull_request_id=self.pull_request_id,
|
|
116
117
|
)
|
|
117
|
-
logger.info(f"Fetched inline comments for {self.
|
|
118
|
+
logger.info(f"Fetched inline comments for {self.pull_request_ref}")
|
|
118
119
|
|
|
119
120
|
return [
|
|
120
|
-
|
|
121
|
-
id=comment.id,
|
|
122
|
-
body=comment.content.raw,
|
|
123
|
-
file=comment.inline.path,
|
|
124
|
-
line=comment.inline.to_line,
|
|
125
|
-
)
|
|
121
|
+
get_review_comment_from_bitbucket_pr_comment(comment)
|
|
126
122
|
for comment in response.values
|
|
127
123
|
if comment.inline is not None
|
|
128
124
|
]
|
|
129
125
|
except Exception as error:
|
|
130
|
-
logger.exception(
|
|
131
|
-
f"Failed to fetch inline comments for "
|
|
132
|
-
f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
133
|
-
)
|
|
126
|
+
logger.exception(f"Failed to fetch inline comments for {self.pull_request_ref}: {error}")
|
|
134
127
|
return []
|
|
135
128
|
|
|
136
129
|
async def create_general_comment(self, message: str) -> None:
|
|
137
130
|
try:
|
|
138
|
-
logger.info(
|
|
139
|
-
f"Posting general comment to PR {self.workspace}/{self.repo_slug}#{self.pull_request_id}: {message}"
|
|
140
|
-
)
|
|
131
|
+
logger.info(f"Posting general comment to PR {self.pull_request_ref}: {message}")
|
|
141
132
|
request = BitbucketCreatePRCommentRequestSchema(
|
|
142
133
|
content=BitbucketCommentContentSchema(raw=message)
|
|
143
134
|
)
|
|
@@ -147,22 +138,14 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
147
138
|
pull_request_id=self.pull_request_id,
|
|
148
139
|
request=request,
|
|
149
140
|
)
|
|
150
|
-
logger.info(
|
|
151
|
-
f"Created general comment in PR {self.workspace}/{self.repo_slug}#{self.pull_request_id}"
|
|
152
|
-
)
|
|
141
|
+
logger.info(f"Created general comment in PR {self.pull_request_ref}")
|
|
153
142
|
except Exception as error:
|
|
154
|
-
logger.exception(
|
|
155
|
-
f"Failed to create general comment in PR "
|
|
156
|
-
f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
157
|
-
)
|
|
143
|
+
logger.exception(f"Failed to create general comment in PR {self.pull_request_ref}: {error}")
|
|
158
144
|
raise
|
|
159
145
|
|
|
160
146
|
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
161
147
|
try:
|
|
162
|
-
logger.info(
|
|
163
|
-
f"Posting inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
|
|
164
|
-
f"at {file}:{line}: {message}"
|
|
165
|
-
)
|
|
148
|
+
logger.info(f"Posting inline comment in {self.pull_request_ref} at {file}:{line}: {message}")
|
|
166
149
|
request = BitbucketCreatePRCommentRequestSchema(
|
|
167
150
|
content=BitbucketCommentContentSchema(raw=message),
|
|
168
151
|
inline=BitbucketCommentInlineSchema(path=file, to_line=line),
|
|
@@ -173,13 +156,106 @@ class BitbucketVCSClient(VCSClientProtocol):
|
|
|
173
156
|
pull_request_id=self.pull_request_id,
|
|
174
157
|
request=request,
|
|
175
158
|
)
|
|
176
|
-
logger.info(
|
|
177
|
-
|
|
178
|
-
|
|
159
|
+
logger.info(f"Created inline comment in {self.pull_request_ref} at {file}:{line}")
|
|
160
|
+
except Exception as error:
|
|
161
|
+
logger.exception(f"Failed to create inline comment in {self.pull_request_ref} at {file}:{line}: {error}")
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
# --- Replies ---
|
|
165
|
+
async def create_inline_reply(self, thread_id: int | str, message: str) -> None:
|
|
166
|
+
try:
|
|
167
|
+
logger.info(f"Replying to inline thread {thread_id=} in PR {self.pull_request_ref}")
|
|
168
|
+
request = BitbucketCreatePRCommentRequestSchema(
|
|
169
|
+
parent=BitbucketParentSchema(id=int(thread_id)),
|
|
170
|
+
content=BitbucketCommentContentSchema(raw=message),
|
|
171
|
+
)
|
|
172
|
+
await self.http_client.pr.create_comment(
|
|
173
|
+
workspace=self.workspace,
|
|
174
|
+
repo_slug=self.repo_slug,
|
|
175
|
+
pull_request_id=self.pull_request_id,
|
|
176
|
+
request=request,
|
|
177
|
+
)
|
|
178
|
+
logger.info(f"Created inline reply to thread {thread_id=} in PR {self.pull_request_ref}")
|
|
179
|
+
except Exception as error:
|
|
180
|
+
logger.exception(
|
|
181
|
+
f"Failed to create inline reply to thread {thread_id=} in PR {self.pull_request_ref}: {error}"
|
|
182
|
+
)
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
async def create_summary_reply(self, thread_id: int | str, message: str) -> None:
|
|
186
|
+
try:
|
|
187
|
+
logger.info(f"Replying to summary thread {thread_id=} in PR {self.pull_request_ref}")
|
|
188
|
+
request = BitbucketCreatePRCommentRequestSchema(
|
|
189
|
+
content=BitbucketCommentContentSchema(raw=message),
|
|
190
|
+
parent=BitbucketParentSchema(id=int(thread_id)),
|
|
191
|
+
)
|
|
192
|
+
await self.http_client.pr.create_comment(
|
|
193
|
+
workspace=self.workspace,
|
|
194
|
+
repo_slug=self.repo_slug,
|
|
195
|
+
pull_request_id=self.pull_request_id,
|
|
196
|
+
request=request,
|
|
179
197
|
)
|
|
198
|
+
logger.info(f"Created summary reply to thread {thread_id=} in PR {self.pull_request_ref}")
|
|
180
199
|
except Exception as error:
|
|
181
200
|
logger.exception(
|
|
182
|
-
f"Failed to create
|
|
183
|
-
f"at {file}:{line}: {error}"
|
|
201
|
+
f"Failed to create summary reply to thread {thread_id=} in PR {self.pull_request_ref}: {error}"
|
|
184
202
|
)
|
|
185
203
|
raise
|
|
204
|
+
|
|
205
|
+
# --- Threads ---
|
|
206
|
+
async def get_inline_threads(self) -> list[ReviewThreadSchema]:
|
|
207
|
+
try:
|
|
208
|
+
comments = await self.get_inline_comments()
|
|
209
|
+
|
|
210
|
+
threads_by_id: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
|
|
211
|
+
for comment in comments:
|
|
212
|
+
if not comment.file:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
threads_by_id[comment.thread_id].append(comment)
|
|
216
|
+
|
|
217
|
+
logger.info(f"Built {len(threads_by_id)} inline threads for {self.pull_request_ref}")
|
|
218
|
+
|
|
219
|
+
threads: list[ReviewThreadSchema] = []
|
|
220
|
+
for thread_id, thread in threads_by_id.items():
|
|
221
|
+
file = thread[0].file
|
|
222
|
+
line = thread[0].line
|
|
223
|
+
if not file:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
threads.append(
|
|
227
|
+
ReviewThreadSchema(
|
|
228
|
+
id=thread_id,
|
|
229
|
+
kind=ThreadKind.INLINE,
|
|
230
|
+
file=file,
|
|
231
|
+
line=line,
|
|
232
|
+
comments=sorted(thread, key=lambda t: int(t.id)),
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return threads
|
|
237
|
+
except Exception as error:
|
|
238
|
+
logger.exception(f"Failed to fetch inline threads for {self.pull_request_ref}: {error}")
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
async def get_general_threads(self) -> list[ReviewThreadSchema]:
|
|
242
|
+
try:
|
|
243
|
+
comments = await self.get_general_comments()
|
|
244
|
+
|
|
245
|
+
threads: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
|
|
246
|
+
for comment in comments:
|
|
247
|
+
threads[comment.thread_id].append(comment)
|
|
248
|
+
|
|
249
|
+
logger.info(f"Built {len(threads)} general threads for {self.pull_request_ref}")
|
|
250
|
+
|
|
251
|
+
return [
|
|
252
|
+
ReviewThreadSchema(
|
|
253
|
+
id=thread_id,
|
|
254
|
+
kind=ThreadKind.SUMMARY,
|
|
255
|
+
comments=sorted(thread, key=lambda c: int(c.id)),
|
|
256
|
+
)
|
|
257
|
+
for thread_id, thread in threads.items()
|
|
258
|
+
]
|
|
259
|
+
except Exception as error:
|
|
260
|
+
logger.exception(f"Failed to fetch general threads for {self.pull_request_ref}: {error}")
|
|
261
|
+
return []
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from ai_review.clients.github.pr.schema.comments import GitHubPRCommentSchema, GitHubIssueCommentSchema
|
|
2
|
+
from ai_review.clients.github.pr.schema.user import GitHubUserSchema
|
|
3
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_user_from_github_user(user: GitHubUserSchema | None) -> UserSchema:
|
|
7
|
+
return UserSchema(
|
|
8
|
+
id=user.id if user else None,
|
|
9
|
+
name=user.login if user else "",
|
|
10
|
+
username=user.login if user else "",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_review_comment_from_github_pr_comment(comment: GitHubPRCommentSchema) -> ReviewCommentSchema:
|
|
15
|
+
parent_id = comment.in_reply_to_id
|
|
16
|
+
thread_id = parent_id or comment.id
|
|
17
|
+
|
|
18
|
+
return ReviewCommentSchema(
|
|
19
|
+
id=comment.id,
|
|
20
|
+
body=comment.body or "",
|
|
21
|
+
file=comment.path,
|
|
22
|
+
line=comment.line,
|
|
23
|
+
author=get_user_from_github_user(comment.user),
|
|
24
|
+
parent_id=parent_id,
|
|
25
|
+
thread_id=thread_id,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_review_comment_from_github_issue_comment(comment: GitHubIssueCommentSchema) -> ReviewCommentSchema:
|
|
30
|
+
return ReviewCommentSchema(
|
|
31
|
+
id=comment.id,
|
|
32
|
+
body=comment.body or "",
|
|
33
|
+
author=get_user_from_github_user(comment.user),
|
|
34
|
+
thread_id=comment.id
|
|
35
|
+
)
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
1
3
|
from ai_review.clients.github.client import get_github_http_client
|
|
2
|
-
from ai_review.clients.github.pr.schema.comments import
|
|
4
|
+
from ai_review.clients.github.pr.schema.comments import (
|
|
5
|
+
GitHubCreateReviewReplyRequestSchema,
|
|
6
|
+
GitHubCreateReviewCommentRequestSchema
|
|
7
|
+
)
|
|
3
8
|
from ai_review.config import settings
|
|
4
9
|
from ai_review.libs.logger import get_logger
|
|
10
|
+
from ai_review.services.vcs.github.adapter import (
|
|
11
|
+
get_review_comment_from_github_pr_comment,
|
|
12
|
+
get_review_comment_from_github_issue_comment
|
|
13
|
+
)
|
|
5
14
|
from ai_review.services.vcs.types import (
|
|
6
15
|
VCSClientProtocol,
|
|
16
|
+
ThreadKind,
|
|
7
17
|
UserSchema,
|
|
8
18
|
BranchRefSchema,
|
|
9
19
|
ReviewInfoSchema,
|
|
20
|
+
ReviewThreadSchema,
|
|
10
21
|
ReviewCommentSchema,
|
|
11
22
|
)
|
|
12
23
|
|
|
@@ -19,7 +30,9 @@ class GitHubVCSClient(VCSClientProtocol):
|
|
|
19
30
|
self.owner = settings.vcs.pipeline.owner
|
|
20
31
|
self.repo = settings.vcs.pipeline.repo
|
|
21
32
|
self.pull_number = settings.vcs.pipeline.pull_number
|
|
33
|
+
self.pull_request_ref = f"{self.owner}/{self.repo}#{self.pull_number}"
|
|
22
34
|
|
|
35
|
+
# --- Review info ---
|
|
23
36
|
async def get_review_info(self) -> ReviewInfoSchema:
|
|
24
37
|
try:
|
|
25
38
|
pr = await self.http_client.pr.get_pull_request(
|
|
@@ -69,7 +82,7 @@ class GitHubVCSClient(VCSClientProtocol):
|
|
|
69
82
|
)
|
|
70
83
|
return ReviewInfoSchema()
|
|
71
84
|
|
|
72
|
-
#
|
|
85
|
+
# --- Comments ---
|
|
73
86
|
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
74
87
|
try:
|
|
75
88
|
response = await self.http_client.pr.get_issue_comments(
|
|
@@ -77,18 +90,11 @@ class GitHubVCSClient(VCSClientProtocol):
|
|
|
77
90
|
repo=self.repo,
|
|
78
91
|
issue_number=self.pull_number,
|
|
79
92
|
)
|
|
80
|
-
logger.info(
|
|
81
|
-
f"Fetched general comments for {self.owner}/{self.repo}#{self.pull_number}"
|
|
82
|
-
)
|
|
93
|
+
logger.info(f"Fetched general comments for {self.pull_request_ref}")
|
|
83
94
|
|
|
84
|
-
return [
|
|
85
|
-
ReviewCommentSchema(id=comment.id, body=comment.body or "")
|
|
86
|
-
for comment in response.root
|
|
87
|
-
]
|
|
95
|
+
return [get_review_comment_from_github_issue_comment(comment) for comment in response.root]
|
|
88
96
|
except Exception as error:
|
|
89
|
-
logger.exception(
|
|
90
|
-
f"Failed to fetch general comments for {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
91
|
-
)
|
|
97
|
+
logger.exception(f"Failed to fetch general comments for {self.pull_request_ref}: {error}")
|
|
92
98
|
return []
|
|
93
99
|
|
|
94
100
|
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
@@ -98,51 +104,30 @@ class GitHubVCSClient(VCSClientProtocol):
|
|
|
98
104
|
repo=self.repo,
|
|
99
105
|
pull_number=self.pull_number,
|
|
100
106
|
)
|
|
101
|
-
logger.info(
|
|
102
|
-
f"Fetched inline comments for {self.owner}/{self.repo}#{self.pull_number}"
|
|
103
|
-
)
|
|
107
|
+
logger.info(f"Fetched inline comments for {self.pull_request_ref}")
|
|
104
108
|
|
|
105
|
-
return [
|
|
106
|
-
ReviewCommentSchema(
|
|
107
|
-
id=comment.id,
|
|
108
|
-
body=comment.body or "",
|
|
109
|
-
file=comment.path,
|
|
110
|
-
line=comment.line,
|
|
111
|
-
)
|
|
112
|
-
for comment in response.root
|
|
113
|
-
]
|
|
109
|
+
return [get_review_comment_from_github_pr_comment(comment) for comment in response.root]
|
|
114
110
|
except Exception as error:
|
|
115
|
-
logger.exception(
|
|
116
|
-
f"Failed to fetch inline comments for {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
117
|
-
)
|
|
111
|
+
logger.exception(f"Failed to fetch inline comments for {self.pull_request_ref}: {error}")
|
|
118
112
|
return []
|
|
119
113
|
|
|
120
114
|
async def create_general_comment(self, message: str) -> None:
|
|
121
115
|
try:
|
|
122
|
-
logger.info(
|
|
123
|
-
f"Posting general comment to PR {self.owner}/{self.repo}#{self.pull_number}: {message}"
|
|
124
|
-
)
|
|
116
|
+
logger.info(f"Posting general comment to PR {self.pull_request_ref}: {message}")
|
|
125
117
|
await self.http_client.pr.create_issue_comment(
|
|
126
118
|
owner=self.owner,
|
|
127
119
|
repo=self.repo,
|
|
128
120
|
issue_number=self.pull_number,
|
|
129
121
|
body=message,
|
|
130
122
|
)
|
|
131
|
-
logger.info(
|
|
132
|
-
f"Created general comment in PR {self.owner}/{self.repo}#{self.pull_number}"
|
|
133
|
-
)
|
|
123
|
+
logger.info(f"Created general comment in PR {self.pull_request_ref}")
|
|
134
124
|
except Exception as error:
|
|
135
|
-
logger.exception(
|
|
136
|
-
f"Failed to create general comment in PR {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
137
|
-
)
|
|
125
|
+
logger.exception(f"Failed to create general comment in PR {self.pull_request_ref}: {error}")
|
|
138
126
|
raise
|
|
139
127
|
|
|
140
128
|
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
141
129
|
try:
|
|
142
|
-
logger.info(
|
|
143
|
-
f"Posting inline comment in {self.owner}/{self.repo}#{self.pull_number} "
|
|
144
|
-
f"at {file}:{line}: {message}"
|
|
145
|
-
)
|
|
130
|
+
logger.info(f"Posting inline comment in {self.pull_request_ref} at {file}:{line}: {message}")
|
|
146
131
|
|
|
147
132
|
pr = await self.http_client.pr.get_pull_request(
|
|
148
133
|
owner=self.owner, repo=self.repo, pull_number=self.pull_number
|
|
@@ -160,12 +145,88 @@ class GitHubVCSClient(VCSClientProtocol):
|
|
|
160
145
|
pull_number=self.pull_number,
|
|
161
146
|
request=request,
|
|
162
147
|
)
|
|
163
|
-
logger.info(
|
|
164
|
-
|
|
148
|
+
logger.info(f"Created inline comment in {self.pull_request_ref} at {file}:{line}")
|
|
149
|
+
except Exception as error:
|
|
150
|
+
logger.exception(f"Failed to create inline comment in {self.pull_request_ref} at {file}:{line}: {error}")
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
# --- Replies ---
|
|
154
|
+
async def create_inline_reply(self, thread_id: int | str, message: str) -> None:
|
|
155
|
+
try:
|
|
156
|
+
logger.info(f"Replying to inline comment {thread_id=} in PR {self.pull_request_ref}")
|
|
157
|
+
request = GitHubCreateReviewReplyRequestSchema(
|
|
158
|
+
body=message,
|
|
159
|
+
in_reply_to=thread_id
|
|
160
|
+
)
|
|
161
|
+
await self.http_client.pr.create_review_reply(
|
|
162
|
+
owner=self.owner,
|
|
163
|
+
repo=self.repo,
|
|
164
|
+
pull_number=self.pull_number,
|
|
165
|
+
request=request,
|
|
165
166
|
)
|
|
167
|
+
logger.info(f"Created inline reply to comment {thread_id=} in PR {self.pull_request_ref}")
|
|
168
|
+
except Exception as error:
|
|
169
|
+
logger.exception(
|
|
170
|
+
f"Failed to create inline reply to comment {thread_id=} in {self.pull_request_ref}: {error}"
|
|
171
|
+
)
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
async def create_summary_reply(self, thread_id: int | str, message: str) -> None:
|
|
175
|
+
"""
|
|
176
|
+
GitHub does not support threaded replies for issue-level comments.
|
|
177
|
+
We post a new top-level comment instead.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
logger.info(f"Replying to general comment {thread_id=} in PR {self.pull_request_ref}")
|
|
181
|
+
await self.create_general_comment(message)
|
|
166
182
|
except Exception as error:
|
|
167
183
|
logger.exception(
|
|
168
|
-
f"Failed to create
|
|
169
|
-
f"at {file}:{line}: {error}"
|
|
184
|
+
f"Failed to create summary reply to comment {thread_id=} in {self.pull_request_ref}: {error}"
|
|
170
185
|
)
|
|
171
186
|
raise
|
|
187
|
+
|
|
188
|
+
# --- Threads ---
|
|
189
|
+
async def get_inline_threads(self) -> list[ReviewThreadSchema]:
|
|
190
|
+
try:
|
|
191
|
+
response = await self.http_client.pr.get_review_comments(
|
|
192
|
+
owner=self.owner,
|
|
193
|
+
repo=self.repo,
|
|
194
|
+
pull_number=self.pull_number,
|
|
195
|
+
)
|
|
196
|
+
comments = response.root
|
|
197
|
+
logger.info(f"Fetched inline comment threads for {self.pull_request_ref}")
|
|
198
|
+
|
|
199
|
+
threads: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
|
|
200
|
+
for comment in comments:
|
|
201
|
+
review_comment = get_review_comment_from_github_pr_comment(comment)
|
|
202
|
+
threads[review_comment.thread_id].append(review_comment)
|
|
203
|
+
|
|
204
|
+
logger.info(f"Built {len(threads)} inline threads for {self.pull_request_ref}")
|
|
205
|
+
|
|
206
|
+
return [
|
|
207
|
+
ReviewThreadSchema(
|
|
208
|
+
id=thread_id,
|
|
209
|
+
kind=ThreadKind.INLINE,
|
|
210
|
+
file=thread[0].file,
|
|
211
|
+
line=thread[0].line,
|
|
212
|
+
comments=sorted(thread, key=lambda t: int(t.id)),
|
|
213
|
+
)
|
|
214
|
+
for thread_id, thread in threads.items()
|
|
215
|
+
]
|
|
216
|
+
except Exception as error:
|
|
217
|
+
logger.exception(f"Failed to fetch inline threads for {self.pull_request_ref}: {error}")
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
async def get_general_threads(self) -> list[ReviewThreadSchema]:
|
|
221
|
+
try:
|
|
222
|
+
comments = await self.get_general_comments()
|
|
223
|
+
|
|
224
|
+
threads = [
|
|
225
|
+
ReviewThreadSchema(id=comment.thread_id, kind=ThreadKind.SUMMARY, comments=[comment])
|
|
226
|
+
for comment in comments
|
|
227
|
+
]
|
|
228
|
+
logger.info(f"Built {len(threads)} general threads for {self.pull_request_ref}")
|
|
229
|
+
return threads
|
|
230
|
+
except Exception as error:
|
|
231
|
+
logger.exception(f"Failed to build general threads for {self.pull_request_ref}: {error}")
|
|
232
|
+
return []
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from ai_review.clients.gitlab.mr.schema.discussions import GitLabDiscussionSchema
|
|
2
|
+
from ai_review.clients.gitlab.mr.schema.notes import GitLabNoteSchema
|
|
3
|
+
from ai_review.clients.gitlab.mr.schema.user import GitLabUserSchema
|
|
4
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_user_from_gitlab_user(user: GitLabUserSchema | None) -> UserSchema:
|
|
8
|
+
return UserSchema(
|
|
9
|
+
id=user.id if user else None,
|
|
10
|
+
name=user.name if user else "",
|
|
11
|
+
username=user.username if user else "",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_review_comment_from_gitlab_note(
|
|
16
|
+
note: GitLabNoteSchema,
|
|
17
|
+
discussion: GitLabDiscussionSchema
|
|
18
|
+
) -> ReviewCommentSchema:
|
|
19
|
+
position = note.position or discussion.position
|
|
20
|
+
|
|
21
|
+
return ReviewCommentSchema(
|
|
22
|
+
id=note.id,
|
|
23
|
+
body=note.body or "",
|
|
24
|
+
file=position.new_path if position else None,
|
|
25
|
+
line=position.new_line if position else None,
|
|
26
|
+
author=get_user_from_gitlab_user(note.author),
|
|
27
|
+
thread_id=discussion.id,
|
|
28
|
+
)
|