xai-review 0.26.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/client.py +45 -8
- ai_review/clients/bitbucket/pr/schema/comments.py +21 -2
- ai_review/clients/bitbucket/pr/schema/files.py +8 -3
- ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
- ai_review/clients/bitbucket/pr/schema/user.py +7 -0
- ai_review/clients/bitbucket/tools.py +6 -0
- ai_review/clients/github/pr/client.py +98 -13
- ai_review/clients/github/pr/schema/comments.py +23 -1
- ai_review/clients/github/pr/schema/files.py +2 -1
- ai_review/clients/github/pr/schema/pull_request.py +1 -4
- ai_review/clients/github/pr/schema/reviews.py +2 -1
- ai_review/clients/github/pr/schema/user.py +6 -0
- ai_review/clients/github/pr/types.py +11 -1
- ai_review/clients/github/tools.py +6 -0
- ai_review/clients/gitlab/mr/client.py +67 -7
- ai_review/clients/gitlab/mr/schema/changes.py +1 -5
- ai_review/clients/gitlab/mr/schema/discussions.py +19 -8
- ai_review/clients/gitlab/mr/schema/notes.py +5 -1
- ai_review/clients/gitlab/mr/schema/user.py +7 -0
- ai_review/clients/gitlab/mr/types.py +16 -7
- ai_review/clients/gitlab/tools.py +5 -0
- ai_review/libs/config/prompt.py +96 -64
- ai_review/libs/config/review.py +2 -0
- ai_review/libs/config/vcs/base.py +2 -0
- ai_review/libs/config/vcs/pagination.py +6 -0
- ai_review/libs/http/paginate.py +43 -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/clients/bitbucket/__init__.py +0 -0
- ai_review/tests/suites/clients/bitbucket/test_client.py +14 -0
- ai_review/tests/suites/clients/bitbucket/test_tools.py +31 -0
- ai_review/tests/suites/clients/github/test_tools.py +31 -0
- ai_review/tests/suites/clients/gitlab/test_tools.py +26 -0
- ai_review/tests/suites/libs/config/test_prompt.py +108 -28
- ai_review/tests/suites/libs/http/__init__.py +0 -0
- ai_review/tests/suites/libs/http/test_paginate.py +95 -0
- 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.26.0.dist-info → xai_review-0.28.0.dist-info}/METADATA +8 -5
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/RECORD +160 -75
- 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.26.0.dist-info → xai_review-0.28.0.dist-info}/WHEEL +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReviewPolicyServiceProtocol(Protocol):
|
|
5
|
+
def should_review_file(self, file: str) -> bool:
|
|
6
|
+
...
|
|
7
|
+
|
|
8
|
+
def apply_for_files(self, files: list[str]) -> list[str]:
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
def apply_for_inline_comments(self, comments: list) -> list:
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
def apply_for_context_comments(self, comments: list) -> list:
|
|
15
|
+
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from ai_review.libs.logger import get_logger
|
|
2
|
-
from ai_review.services.review.summary.schema import SummaryCommentSchema
|
|
3
|
-
from ai_review.services.review.summary.types import SummaryCommentServiceProtocol
|
|
2
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
3
|
+
from ai_review.services.review.internal.summary.types import SummaryCommentServiceProtocol
|
|
4
4
|
|
|
5
5
|
logger = get_logger("SUMMARY_COMMENT_SERVICE")
|
|
6
6
|
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ai_review.config import settings
|
|
2
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SummaryCommentReplySchema(SummaryCommentSchema):
|
|
6
|
+
@property
|
|
7
|
+
def body_with_tag(self):
|
|
8
|
+
return f"{self.text}\n\n{settings.review.summary_reply_tag}"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ai_review.libs.logger import get_logger
|
|
2
|
+
from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
|
|
3
|
+
from ai_review.services.review.internal.summary_reply.types import SummaryCommentReplyServiceProtocol
|
|
4
|
+
|
|
5
|
+
logger = get_logger("SUMMARY_COMMENT_REPLY_SERVICE")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SummaryCommentReplyService(SummaryCommentReplyServiceProtocol):
|
|
9
|
+
@classmethod
|
|
10
|
+
def parse_model_output(cls, output: str) -> SummaryCommentReplySchema:
|
|
11
|
+
text = (output or "").strip()
|
|
12
|
+
if not text:
|
|
13
|
+
logger.warning("LLM returned empty summary")
|
|
14
|
+
|
|
15
|
+
return SummaryCommentReplySchema(text=text)
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
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.inline.types import InlineCommentServiceProtocol
|
|
10
|
+
from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
|
|
11
|
+
from ai_review.services.review.runner.types import ReviewRunnerProtocol
|
|
12
|
+
from ai_review.services.vcs.types import VCSClientProtocol
|
|
13
|
+
|
|
14
|
+
logger = get_logger("CONTEXT_REVIEW_RUNNER")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContextReviewRunner(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
|
+
inline_comment: InlineCommentServiceProtocol,
|
|
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.inline_comment = inline_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_context_review_start()
|
|
42
|
+
if await self.review_comment_gateway.has_existing_inline_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 context review")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
logger.info(f"Starting context inline 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_context_request(rendered_files, prompt_context)
|
|
61
|
+
prompt_system = self.prompt.build_system_context_request(prompt_context)
|
|
62
|
+
prompt_result = await self.review_llm_gateway.ask(prompt, prompt_system)
|
|
63
|
+
|
|
64
|
+
comments = self.inline_comment.parse_model_output(prompt_result).dedupe()
|
|
65
|
+
comments.root = self.review_policy.apply_for_context_comments(comments.root)
|
|
66
|
+
if not comments.root:
|
|
67
|
+
logger.info("No inline comments from context review")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
logger.info(f"Posting {len(comments.root)} inline comments (context review)")
|
|
71
|
+
await self.review_comment_gateway.process_inline_comments(comments)
|
|
72
|
+
await hook.emit_context_review_complete(self.cost.aggregate())
|
|
@@ -0,0 +1,80 @@
|
|
|
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 ReviewLLMGatewayProtocol, ReviewCommentGatewayProtocol
|
|
10
|
+
from ai_review.services.review.internal.inline.types import InlineCommentServiceProtocol
|
|
11
|
+
from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
|
|
12
|
+
from ai_review.services.review.runner.types import ReviewRunnerProtocol
|
|
13
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, VCSClientProtocol
|
|
14
|
+
|
|
15
|
+
logger = get_logger("INLINE_REVIEW_RUNNER")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InlineReviewRunner(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
|
+
inline_comment: InlineCommentServiceProtocol,
|
|
28
|
+
review_llm_gateway: ReviewLLMGatewayProtocol,
|
|
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.inline_comment = inline_comment
|
|
38
|
+
self.review_llm_gateway = review_llm_gateway
|
|
39
|
+
self.review_comment_gateway = review_comment_gateway
|
|
40
|
+
|
|
41
|
+
async def process_file(self, file: str, review_info: ReviewInfoSchema) -> None:
|
|
42
|
+
raw_diff = self.git.get_diff_for_file(review_info.base_sha, review_info.head_sha, file)
|
|
43
|
+
if not raw_diff.strip():
|
|
44
|
+
logger.debug(f"No diff for {file}, skipping")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
rendered_file = self.diff.render_file(
|
|
48
|
+
file=file,
|
|
49
|
+
base_sha=review_info.base_sha,
|
|
50
|
+
head_sha=review_info.head_sha,
|
|
51
|
+
raw_diff=raw_diff,
|
|
52
|
+
)
|
|
53
|
+
prompt_context = build_prompt_context_from_review_info(review_info)
|
|
54
|
+
prompt = self.prompt.build_inline_request(rendered_file, prompt_context)
|
|
55
|
+
prompt_system = self.prompt.build_system_inline_request(prompt_context)
|
|
56
|
+
prompt_result = await self.review_llm_gateway.ask(prompt, prompt_system)
|
|
57
|
+
|
|
58
|
+
comments = self.inline_comment.parse_model_output(prompt_result).dedupe()
|
|
59
|
+
comments.root = self.review_policy.apply_for_inline_comments(comments.root)
|
|
60
|
+
if not comments.root:
|
|
61
|
+
logger.info(f"No inline comments for file: {file}")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
logger.info(f"Posting {len(comments.root)} inline comments to {file}")
|
|
65
|
+
await self.review_comment_gateway.process_inline_comments(comments)
|
|
66
|
+
|
|
67
|
+
async def run(self) -> None:
|
|
68
|
+
await hook.emit_inline_review_start()
|
|
69
|
+
if await self.review_comment_gateway.has_existing_inline_comments():
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
review_info = await self.vcs.get_review_info()
|
|
73
|
+
logger.info(f"Starting inline review: {len(review_info.changed_files)} files changed")
|
|
74
|
+
|
|
75
|
+
changed_files = self.review_policy.apply_for_files(review_info.changed_files)
|
|
76
|
+
await bounded_gather([
|
|
77
|
+
self.process_file(changed_file, review_info)
|
|
78
|
+
for changed_file in changed_files
|
|
79
|
+
])
|
|
80
|
+
await hook.emit_inline_review_complete(self.cost.aggregate())
|
|
@@ -0,0 +1,80 @@
|
|
|
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.inline_reply.types import InlineCommentReplyServiceProtocol
|
|
11
|
+
from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
|
|
12
|
+
from ai_review.services.review.runner.types import ReviewRunnerProtocol
|
|
13
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, VCSClientProtocol, ReviewThreadSchema
|
|
14
|
+
|
|
15
|
+
logger = get_logger("INLINE_REPLY_REVIEW_RUNNER")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InlineReplyReviewRunner(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
|
+
inline_comment_reply: InlineCommentReplyServiceProtocol,
|
|
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.inline_comment_reply = inline_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 inline reply for thread {thread.id}")
|
|
43
|
+
|
|
44
|
+
raw_diff = self.git.get_diff_for_file(review_info.base_sha, review_info.head_sha, thread.file)
|
|
45
|
+
if not raw_diff.strip():
|
|
46
|
+
logger.debug(f"No diff for {thread.file}, skipping")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
rendered_file = self.diff.render_file(
|
|
50
|
+
file=thread.file,
|
|
51
|
+
base_sha=review_info.base_sha,
|
|
52
|
+
head_sha=review_info.head_sha,
|
|
53
|
+
raw_diff=raw_diff
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
prompt_context = build_prompt_context_from_review_info(review_info)
|
|
57
|
+
prompt = self.prompt.build_inline_reply_request(rendered_file, thread, prompt_context)
|
|
58
|
+
prompt_system = self.prompt.build_system_inline_reply_request(prompt_context)
|
|
59
|
+
prompt_result = await self.review_llm_gateway.ask(prompt, prompt_system)
|
|
60
|
+
|
|
61
|
+
reply = self.inline_comment_reply.parse_model_output(prompt_result)
|
|
62
|
+
if not reply:
|
|
63
|
+
logger.info(f"AI model returned no valid reply for thread {thread.id} ({len(thread.comments)} comments)")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
await self.review_comment_gateway.process_inline_reply(thread.id, reply)
|
|
67
|
+
|
|
68
|
+
async def run(self) -> None:
|
|
69
|
+
await hook.emit_inline_reply_review_start()
|
|
70
|
+
|
|
71
|
+
review_info = await self.vcs.get_review_info()
|
|
72
|
+
threads = await self.review_comment_gateway.get_inline_threads()
|
|
73
|
+
if not threads:
|
|
74
|
+
logger.info("No AI inline threads found, skipping reply mode")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
logger.info(f"Found {len(threads)} AI inline threads for reply")
|
|
78
|
+
|
|
79
|
+
await bounded_gather([self.process_thread_reply(thread, review_info) for thread in threads])
|
|
80
|
+
await hook.emit_inline_reply_review_complete(self.cost.aggregate())
|
|
@@ -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())
|