xai-review 0.27.0__py3-none-any.whl → 0.28.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 +17 -7
- ai_review/clients/gitlab/mr/schema/notes.py +3 -0
- ai_review/clients/gitlab/mr/schema/user.py +7 -0
- ai_review/clients/gitlab/mr/types.py +16 -7
- 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/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 +24 -0
- ai_review/services/vcs/bitbucket/client.py +107 -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 +26 -0
- ai_review/services/vcs/gitlab/client.py +91 -38
- 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 +42 -3
- 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 +105 -0
- ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +99 -1
- {xai_review-0.27.0.dist-info → xai_review-0.28.0.dist-info}/METADATA +8 -5
- {xai_review-0.27.0.dist-info → xai_review-0.28.0.dist-info}/RECORD +143 -70
- 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.28.0.dist-info}/WHEEL +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.28.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.28.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from ai_review.libs.logger import get_logger
|
|
2
|
+
from ai_review.services.cost.types import CostServiceProtocol
|
|
3
|
+
from ai_review.services.diff.types import DiffServiceProtocol
|
|
4
|
+
from ai_review.services.git.types import GitServiceProtocol
|
|
5
|
+
from ai_review.services.hook import hook
|
|
6
|
+
from ai_review.services.prompt.adapter import build_prompt_context_from_review_info
|
|
7
|
+
from ai_review.services.prompt.types import PromptServiceProtocol
|
|
8
|
+
from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol, ReviewCommentGatewayProtocol
|
|
9
|
+
from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
|
|
10
|
+
from ai_review.services.review.internal.summary.types import SummaryCommentServiceProtocol
|
|
11
|
+
from ai_review.services.review.runner.types import ReviewRunnerProtocol
|
|
12
|
+
from ai_review.services.vcs.types import VCSClientProtocol
|
|
13
|
+
|
|
14
|
+
logger = get_logger("SUMMARY_REVIEW_RUNNER")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SummaryReviewRunner(ReviewRunnerProtocol):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
vcs: VCSClientProtocol,
|
|
21
|
+
git: GitServiceProtocol,
|
|
22
|
+
diff: DiffServiceProtocol,
|
|
23
|
+
cost: CostServiceProtocol,
|
|
24
|
+
prompt: PromptServiceProtocol,
|
|
25
|
+
review_policy: ReviewPolicyServiceProtocol,
|
|
26
|
+
summary_comment: SummaryCommentServiceProtocol,
|
|
27
|
+
review_llm_gateway: ReviewLLMGatewayProtocol,
|
|
28
|
+
review_comment_gateway: ReviewCommentGatewayProtocol,
|
|
29
|
+
):
|
|
30
|
+
self.vcs = vcs
|
|
31
|
+
self.git = git
|
|
32
|
+
self.diff = diff
|
|
33
|
+
self.cost = cost
|
|
34
|
+
self.prompt = prompt
|
|
35
|
+
self.review_policy = review_policy
|
|
36
|
+
self.summary_comment = summary_comment
|
|
37
|
+
self.review_llm_gateway = review_llm_gateway
|
|
38
|
+
self.review_comment_gateway = review_comment_gateway
|
|
39
|
+
|
|
40
|
+
async def run(self) -> None:
|
|
41
|
+
await hook.emit_summary_review_start()
|
|
42
|
+
if await self.review_comment_gateway.has_existing_summary_comments():
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
review_info = await self.vcs.get_review_info()
|
|
46
|
+
changed_files = self.review_policy.apply_for_files(review_info.changed_files)
|
|
47
|
+
if not changed_files:
|
|
48
|
+
logger.info("No files to review for summary")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
logger.info(f"Starting summary review: {len(changed_files)} files changed")
|
|
52
|
+
|
|
53
|
+
rendered_files = self.diff.render_files(
|
|
54
|
+
git=self.git,
|
|
55
|
+
files=changed_files,
|
|
56
|
+
base_sha=review_info.base_sha,
|
|
57
|
+
head_sha=review_info.head_sha,
|
|
58
|
+
)
|
|
59
|
+
prompt_context = build_prompt_context_from_review_info(review_info)
|
|
60
|
+
prompt = self.prompt.build_summary_request(rendered_files, prompt_context)
|
|
61
|
+
prompt_system = self.prompt.build_system_summary_request(prompt_context)
|
|
62
|
+
prompt_result = await self.review_llm_gateway.ask(prompt, prompt_system)
|
|
63
|
+
|
|
64
|
+
summary = self.summary_comment.parse_model_output(prompt_result)
|
|
65
|
+
if not summary.text.strip():
|
|
66
|
+
logger.warning("Summary LLM output was empty, skipping comment")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
logger.info(f"Posting summary review comment ({len(summary.text)} chars)")
|
|
70
|
+
await self.review_comment_gateway.process_summary_comment(summary)
|
|
71
|
+
await hook.emit_summary_review_complete(self.cost.aggregate())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from ai_review.libs.asynchronous.gather import bounded_gather
|
|
2
|
+
from ai_review.libs.logger import get_logger
|
|
3
|
+
from ai_review.services.cost.types import CostServiceProtocol
|
|
4
|
+
from ai_review.services.diff.types import DiffServiceProtocol
|
|
5
|
+
from ai_review.services.git.types import GitServiceProtocol
|
|
6
|
+
from ai_review.services.hook import hook
|
|
7
|
+
from ai_review.services.prompt.adapter import build_prompt_context_from_review_info
|
|
8
|
+
from ai_review.services.prompt.types import PromptServiceProtocol
|
|
9
|
+
from ai_review.services.review.gateway.types import ReviewCommentGatewayProtocol, ReviewLLMGatewayProtocol
|
|
10
|
+
from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
|
|
11
|
+
from ai_review.services.review.internal.summary_reply.types import SummaryCommentReplyServiceProtocol
|
|
12
|
+
from ai_review.services.review.runner.types import ReviewRunnerProtocol
|
|
13
|
+
from ai_review.services.vcs.types import VCSClientProtocol, ReviewThreadSchema, ReviewInfoSchema
|
|
14
|
+
|
|
15
|
+
logger = get_logger("SUMMARY_REPLY_REVIEW_RUNNER")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SummaryReplyReviewRunner(ReviewRunnerProtocol):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
vcs: VCSClientProtocol,
|
|
22
|
+
git: GitServiceProtocol,
|
|
23
|
+
diff: DiffServiceProtocol,
|
|
24
|
+
cost: CostServiceProtocol,
|
|
25
|
+
prompt: PromptServiceProtocol,
|
|
26
|
+
review_policy: ReviewPolicyServiceProtocol,
|
|
27
|
+
review_llm_gateway: ReviewLLMGatewayProtocol,
|
|
28
|
+
summary_comment_reply: SummaryCommentReplyServiceProtocol,
|
|
29
|
+
review_comment_gateway: ReviewCommentGatewayProtocol,
|
|
30
|
+
):
|
|
31
|
+
self.vcs = vcs
|
|
32
|
+
self.git = git
|
|
33
|
+
self.diff = diff
|
|
34
|
+
self.cost = cost
|
|
35
|
+
self.prompt = prompt
|
|
36
|
+
self.review_policy = review_policy
|
|
37
|
+
self.review_llm_gateway = review_llm_gateway
|
|
38
|
+
self.summary_comment_reply = summary_comment_reply
|
|
39
|
+
self.review_comment_gateway = review_comment_gateway
|
|
40
|
+
|
|
41
|
+
async def process_thread_reply(self, thread: ReviewThreadSchema, review_info: ReviewInfoSchema):
|
|
42
|
+
logger.info(f"Processing summary reply for thread {thread.id}")
|
|
43
|
+
|
|
44
|
+
changed_files = self.review_policy.apply_for_files(review_info.changed_files)
|
|
45
|
+
if not changed_files:
|
|
46
|
+
logger.info("No files to review for summary")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
rendered_files = self.diff.render_files(
|
|
50
|
+
git=self.git,
|
|
51
|
+
files=changed_files,
|
|
52
|
+
base_sha=review_info.base_sha,
|
|
53
|
+
head_sha=review_info.head_sha,
|
|
54
|
+
)
|
|
55
|
+
prompt_context = build_prompt_context_from_review_info(review_info)
|
|
56
|
+
prompt = self.prompt.build_summary_reply_request(rendered_files, thread, prompt_context)
|
|
57
|
+
prompt_system = self.prompt.build_system_summary_reply_request(prompt_context)
|
|
58
|
+
prompt_result = await self.review_llm_gateway.ask(prompt, prompt_system)
|
|
59
|
+
|
|
60
|
+
reply = self.summary_comment_reply.parse_model_output(prompt_result)
|
|
61
|
+
if not reply:
|
|
62
|
+
logger.info(f"No valid reply generated for summary thread {thread.id}")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
await self.review_comment_gateway.process_summary_reply(thread.id, reply)
|
|
66
|
+
|
|
67
|
+
async def run(self) -> None:
|
|
68
|
+
await hook.emit_summary_reply_review_start()
|
|
69
|
+
|
|
70
|
+
review_info = await self.vcs.get_review_info()
|
|
71
|
+
threads = await self.review_comment_gateway.get_summary_threads()
|
|
72
|
+
if not threads:
|
|
73
|
+
logger.info("No AI summary threads found, skipping summary reply mode")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
logger.info(f"Found {len(threads)} AI summary threads for reply")
|
|
77
|
+
|
|
78
|
+
await bounded_gather([self.process_thread_reply(thread, review_info) for thread in threads])
|
|
79
|
+
await hook.emit_summary_reply_review_complete(self.cost.aggregate())
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
from ai_review.libs.asynchronous.gather import bounded_gather
|
|
2
1
|
from ai_review.libs.logger import get_logger
|
|
3
2
|
from ai_review.services.artifacts.service import ArtifactsService
|
|
4
3
|
from ai_review.services.cost.service import CostService
|
|
5
4
|
from ai_review.services.diff.service import DiffService
|
|
6
5
|
from ai_review.services.git.service import GitService
|
|
7
|
-
from ai_review.services.hook import hook
|
|
8
6
|
from ai_review.services.llm.factory import get_llm_client
|
|
9
|
-
from ai_review.services.prompt.adapter import build_prompt_context_from_mr_info
|
|
10
7
|
from ai_review.services.prompt.service import PromptService
|
|
11
8
|
from ai_review.services.review.gateway.comment import ReviewCommentGateway
|
|
12
9
|
from ai_review.services.review.gateway.llm import ReviewLLMGateway
|
|
13
|
-
from ai_review.services.review.inline.service import InlineCommentService
|
|
14
|
-
from ai_review.services.review.
|
|
15
|
-
from ai_review.services.review.
|
|
10
|
+
from ai_review.services.review.internal.inline.service import InlineCommentService
|
|
11
|
+
from ai_review.services.review.internal.inline_reply.service import InlineCommentReplyService
|
|
12
|
+
from ai_review.services.review.internal.policy.service import ReviewPolicyService
|
|
13
|
+
from ai_review.services.review.internal.summary.service import SummaryCommentService
|
|
14
|
+
from ai_review.services.review.internal.summary_reply.service import SummaryCommentReplyService
|
|
15
|
+
from ai_review.services.review.runner.context import ContextReviewRunner
|
|
16
|
+
from ai_review.services.review.runner.inline import InlineReviewRunner
|
|
17
|
+
from ai_review.services.review.runner.inline_reply import InlineReplyReviewRunner
|
|
18
|
+
from ai_review.services.review.runner.summary import SummaryReviewRunner
|
|
19
|
+
from ai_review.services.review.runner.summary_reply import SummaryReplyReviewRunner
|
|
16
20
|
from ai_review.services.vcs.factory import get_vcs_client
|
|
17
|
-
from ai_review.services.vcs.types import ReviewInfoSchema
|
|
18
21
|
|
|
19
22
|
logger = get_logger("REVIEW_SERVICE")
|
|
20
23
|
|
|
@@ -27,125 +30,90 @@ class ReviewService:
|
|
|
27
30
|
self.diff = DiffService()
|
|
28
31
|
self.cost = CostService()
|
|
29
32
|
self.prompt = PromptService()
|
|
30
|
-
self.policy = ReviewPolicyService()
|
|
31
|
-
self.inline = InlineCommentService()
|
|
32
|
-
self.summary = SummaryCommentService()
|
|
33
33
|
self.artifacts = ArtifactsService()
|
|
34
|
+
self.review_policy = ReviewPolicyService()
|
|
35
|
+
self.inline_comment = InlineCommentService()
|
|
36
|
+
self.summary_comment = SummaryCommentService()
|
|
37
|
+
self.inline_comment_reply = InlineCommentReplyService()
|
|
38
|
+
self.summary_comment_reply = SummaryCommentReplyService()
|
|
34
39
|
|
|
35
|
-
self.
|
|
40
|
+
self.review_llm_gateway = ReviewLLMGateway(
|
|
36
41
|
llm=self.llm,
|
|
37
42
|
cost=self.cost,
|
|
38
43
|
artifacts=self.artifacts
|
|
39
44
|
)
|
|
40
|
-
self.
|
|
45
|
+
self.review_comment_gateway = ReviewCommentGateway(vcs=self.vcs)
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
raw_diff=raw_diff,
|
|
47
|
+
self.inline_review_runner = InlineReviewRunner(
|
|
48
|
+
vcs=self.vcs,
|
|
49
|
+
git=self.git,
|
|
50
|
+
diff=self.diff,
|
|
51
|
+
cost=self.cost,
|
|
52
|
+
prompt=self.prompt,
|
|
53
|
+
review_policy=self.review_policy,
|
|
54
|
+
inline_comment=self.inline_comment,
|
|
55
|
+
review_llm_gateway=self.review_llm_gateway,
|
|
56
|
+
review_comment_gateway=self.review_comment_gateway
|
|
53
57
|
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
prompt_system = self.prompt.build_system_inline_request(prompt_context)
|
|
57
|
-
prompt_result = await self.llm_gateway.ask(prompt, prompt_system)
|
|
58
|
-
|
|
59
|
-
comments = self.inline.parse_model_output(prompt_result).dedupe()
|
|
60
|
-
comments.root = self.policy.apply_for_inline_comments(comments.root)
|
|
61
|
-
if not comments.root:
|
|
62
|
-
logger.info(f"No inline comments for file: {file}")
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
logger.info(f"Posting {len(comments.root)} inline comments to {file}")
|
|
66
|
-
await self.comment_gateway.process_inline_comments(comments)
|
|
67
|
-
|
|
68
|
-
async def run_inline_review(self) -> None:
|
|
69
|
-
await hook.emit_inline_review_start()
|
|
70
|
-
if await self.comment_gateway.has_existing_inline_comments():
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
review_info = await self.vcs.get_review_info()
|
|
74
|
-
logger.info(f"Starting inline review: {len(review_info.changed_files)} files changed")
|
|
75
|
-
|
|
76
|
-
changed_files = self.policy.apply_for_files(review_info.changed_files)
|
|
77
|
-
await bounded_gather([
|
|
78
|
-
self.process_file_inline(changed_file, review_info)
|
|
79
|
-
for changed_file in changed_files
|
|
80
|
-
])
|
|
81
|
-
await hook.emit_inline_review_complete(self.cost.aggregate())
|
|
82
|
-
|
|
83
|
-
async def run_context_review(self) -> None:
|
|
84
|
-
await hook.emit_context_review_start()
|
|
85
|
-
if await self.comment_gateway.has_existing_inline_comments():
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
review_info = await self.vcs.get_review_info()
|
|
89
|
-
changed_files = self.policy.apply_for_files(review_info.changed_files)
|
|
90
|
-
if not changed_files:
|
|
91
|
-
logger.info("No files to review for context review")
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
logger.info(f"Starting context inline review: {len(changed_files)} files changed")
|
|
95
|
-
|
|
96
|
-
rendered_files = self.diff.render_files(
|
|
58
|
+
self.context_review_runner = ContextReviewRunner(
|
|
59
|
+
vcs=self.vcs,
|
|
97
60
|
git=self.git,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
61
|
+
diff=self.diff,
|
|
62
|
+
cost=self.cost,
|
|
63
|
+
prompt=self.prompt,
|
|
64
|
+
review_policy=self.review_policy,
|
|
65
|
+
inline_comment=self.inline_comment,
|
|
66
|
+
review_llm_gateway=self.review_llm_gateway,
|
|
67
|
+
review_comment_gateway=self.review_comment_gateway
|
|
68
|
+
)
|
|
69
|
+
self.summary_review_runner = SummaryReviewRunner(
|
|
70
|
+
vcs=self.vcs,
|
|
71
|
+
git=self.git,
|
|
72
|
+
diff=self.diff,
|
|
73
|
+
cost=self.cost,
|
|
74
|
+
prompt=self.prompt,
|
|
75
|
+
review_policy=self.review_policy,
|
|
76
|
+
summary_comment=self.summary_comment,
|
|
77
|
+
review_llm_gateway=self.review_llm_gateway,
|
|
78
|
+
review_comment_gateway=self.review_comment_gateway
|
|
79
|
+
)
|
|
80
|
+
self.inline_reply_review_runner = InlineReplyReviewRunner(
|
|
81
|
+
vcs=self.vcs,
|
|
82
|
+
git=self.git,
|
|
83
|
+
diff=self.diff,
|
|
84
|
+
cost=self.cost,
|
|
85
|
+
prompt=self.prompt,
|
|
86
|
+
review_policy=self.review_policy,
|
|
87
|
+
review_llm_gateway=self.review_llm_gateway,
|
|
88
|
+
inline_comment_reply=self.inline_comment_reply,
|
|
89
|
+
review_comment_gateway=self.review_comment_gateway
|
|
90
|
+
)
|
|
91
|
+
self.summary_reply_review_runner = SummaryReplyReviewRunner(
|
|
92
|
+
vcs=self.vcs,
|
|
93
|
+
git=self.git,
|
|
94
|
+
diff=self.diff,
|
|
95
|
+
cost=self.cost,
|
|
96
|
+
prompt=self.prompt,
|
|
97
|
+
review_policy=self.review_policy,
|
|
98
|
+
review_llm_gateway=self.review_llm_gateway,
|
|
99
|
+
summary_comment_reply=self.summary_comment_reply,
|
|
100
|
+
review_comment_gateway=self.review_comment_gateway
|
|
101
101
|
)
|
|
102
|
-
prompt_context = build_prompt_context_from_mr_info(review_info)
|
|
103
|
-
prompt = self.prompt.build_context_request(rendered_files, prompt_context)
|
|
104
|
-
prompt_system = self.prompt.build_system_context_request(prompt_context)
|
|
105
|
-
prompt_result = await self.llm_gateway.ask(prompt, prompt_system)
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if not comments.root:
|
|
110
|
-
logger.info("No inline comments from context review")
|
|
111
|
-
return
|
|
103
|
+
async def run_inline_review(self) -> None:
|
|
104
|
+
await self.inline_review_runner.run()
|
|
112
105
|
|
|
113
|
-
|
|
114
|
-
await self.
|
|
115
|
-
await hook.emit_context_review_complete(self.cost.aggregate())
|
|
106
|
+
async def run_context_review(self) -> None:
|
|
107
|
+
await self.context_review_runner.run()
|
|
116
108
|
|
|
117
109
|
async def run_summary_review(self) -> None:
|
|
118
|
-
await
|
|
119
|
-
if await self.comment_gateway.has_existing_summary_comments():
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
review_info = await self.vcs.get_review_info()
|
|
123
|
-
changed_files = self.policy.apply_for_files(review_info.changed_files)
|
|
124
|
-
if not changed_files:
|
|
125
|
-
logger.info("No files to review for summary")
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
logger.info(f"Starting summary review: {len(changed_files)} files changed")
|
|
129
|
-
|
|
130
|
-
rendered_files = self.diff.render_files(
|
|
131
|
-
git=self.git,
|
|
132
|
-
files=changed_files,
|
|
133
|
-
base_sha=review_info.base_sha,
|
|
134
|
-
head_sha=review_info.head_sha,
|
|
135
|
-
)
|
|
136
|
-
prompt_context = build_prompt_context_from_mr_info(review_info)
|
|
137
|
-
prompt = self.prompt.build_summary_request(rendered_files, prompt_context)
|
|
138
|
-
prompt_system = self.prompt.build_system_summary_request(prompt_context)
|
|
139
|
-
prompt_result = await self.llm_gateway.ask(prompt, prompt_system)
|
|
110
|
+
await self.summary_review_runner.run()
|
|
140
111
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
logger.warning("Summary LLM output was empty, skipping comment")
|
|
144
|
-
return
|
|
112
|
+
async def run_inline_reply_review(self) -> None:
|
|
113
|
+
await self.inline_reply_review_runner.run()
|
|
145
114
|
|
|
146
|
-
|
|
147
|
-
await self.
|
|
148
|
-
await hook.emit_summary_review_complete(self.cost.aggregate())
|
|
115
|
+
async def run_summary_reply_review(self) -> None:
|
|
116
|
+
await self.summary_reply_review_runner.run()
|
|
149
117
|
|
|
150
118
|
def report_total_cost(self):
|
|
151
119
|
total_report = self.cost.aggregate()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ai_review.clients.bitbucket.pr.schema.comments import BitbucketPRCommentSchema
|
|
2
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_review_comment_from_bitbucket_pr_comment(comment: BitbucketPRCommentSchema) -> ReviewCommentSchema:
|
|
6
|
+
parent_id = comment.parent.id if comment.parent else None
|
|
7
|
+
thread_id = parent_id or comment.id
|
|
8
|
+
|
|
9
|
+
user = comment.user
|
|
10
|
+
author = UserSchema(
|
|
11
|
+
id=user.uuid if user else None,
|
|
12
|
+
name=user.display_name if user else "",
|
|
13
|
+
username=user.nickname if user else "",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return ReviewCommentSchema(
|
|
17
|
+
id=comment.id,
|
|
18
|
+
body=comment.content.raw or "",
|
|
19
|
+
file=comment.inline.path if comment.inline else None,
|
|
20
|
+
line=comment.inline.to_line if comment.inline else None,
|
|
21
|
+
author=author,
|
|
22
|
+
parent_id=parent_id,
|
|
23
|
+
thread_id=thread_id,
|
|
24
|
+
)
|
|
@@ -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,95 @@ 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: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
|
|
211
|
+
for comment in comments:
|
|
212
|
+
threads[comment.thread_id].append(comment)
|
|
213
|
+
|
|
214
|
+
logger.info(f"Built {len(threads)} inline threads for {self.pull_request_ref}")
|
|
215
|
+
|
|
216
|
+
return [
|
|
217
|
+
ReviewThreadSchema(
|
|
218
|
+
id=thread_id,
|
|
219
|
+
kind=ThreadKind.INLINE,
|
|
220
|
+
file=thread[0].file,
|
|
221
|
+
line=thread[0].line,
|
|
222
|
+
comments=sorted(thread, key=lambda c: int(c.id)),
|
|
223
|
+
)
|
|
224
|
+
for thread_id, thread in threads.items()
|
|
225
|
+
]
|
|
226
|
+
except Exception as error:
|
|
227
|
+
logger.exception(f"Failed to fetch inline threads for {self.pull_request_ref}: {error}")
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
async def get_general_threads(self) -> list[ReviewThreadSchema]:
|
|
231
|
+
try:
|
|
232
|
+
comments = await self.get_general_comments()
|
|
233
|
+
|
|
234
|
+
threads: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
|
|
235
|
+
for comment in comments:
|
|
236
|
+
threads[comment.thread_id].append(comment)
|
|
237
|
+
|
|
238
|
+
logger.info(f"Built {len(threads)} general threads for {self.pull_request_ref}")
|
|
239
|
+
|
|
240
|
+
return [
|
|
241
|
+
ReviewThreadSchema(
|
|
242
|
+
id=thread_id,
|
|
243
|
+
kind=ThreadKind.SUMMARY,
|
|
244
|
+
comments=sorted(thread, key=lambda c: int(c.id)),
|
|
245
|
+
)
|
|
246
|
+
for thread_id, thread in threads.items()
|
|
247
|
+
]
|
|
248
|
+
except Exception as error:
|
|
249
|
+
logger.exception(f"Failed to fetch general threads for {self.pull_request_ref}: {error}")
|
|
250
|
+
return []
|