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.

Files changed (150) hide show
  1. ai_review/cli/commands/run_inline_reply_review.py +7 -0
  2. ai_review/cli/commands/run_summary_reply_review.py +7 -0
  3. ai_review/cli/main.py +17 -0
  4. ai_review/clients/bitbucket/pr/schema/comments.py +14 -0
  5. ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
  6. ai_review/clients/bitbucket/pr/schema/user.py +7 -0
  7. ai_review/clients/github/pr/client.py +35 -4
  8. ai_review/clients/github/pr/schema/comments.py +21 -0
  9. ai_review/clients/github/pr/schema/pull_request.py +1 -4
  10. ai_review/clients/github/pr/schema/user.py +6 -0
  11. ai_review/clients/github/pr/types.py +11 -1
  12. ai_review/clients/gitlab/mr/client.py +32 -1
  13. ai_review/clients/gitlab/mr/schema/changes.py +1 -5
  14. ai_review/clients/gitlab/mr/schema/discussions.py +14 -12
  15. ai_review/clients/gitlab/mr/schema/notes.py +5 -0
  16. ai_review/clients/gitlab/mr/schema/position.py +13 -0
  17. ai_review/clients/gitlab/mr/schema/user.py +7 -0
  18. ai_review/clients/gitlab/mr/types.py +16 -7
  19. ai_review/libs/asynchronous/gather.py +8 -1
  20. ai_review/libs/config/prompt.py +96 -64
  21. ai_review/libs/config/review.py +2 -0
  22. ai_review/libs/llm/output_json_parser.py +60 -0
  23. ai_review/prompts/default_inline_reply.md +10 -0
  24. ai_review/prompts/default_summary_reply.md +14 -0
  25. ai_review/prompts/default_system_inline_reply.md +31 -0
  26. ai_review/prompts/default_system_summary_reply.md +13 -0
  27. ai_review/services/artifacts/schema.py +2 -2
  28. ai_review/services/git/service.py +42 -11
  29. ai_review/services/hook/constants.py +14 -0
  30. ai_review/services/hook/service.py +95 -4
  31. ai_review/services/hook/types.py +18 -2
  32. ai_review/services/prompt/adapter.py +1 -1
  33. ai_review/services/prompt/service.py +49 -3
  34. ai_review/services/prompt/tools.py +21 -0
  35. ai_review/services/prompt/types.py +23 -0
  36. ai_review/services/review/gateway/comment.py +45 -6
  37. ai_review/services/review/gateway/llm.py +2 -1
  38. ai_review/services/review/gateway/types.py +50 -0
  39. ai_review/services/review/internal/inline/service.py +40 -0
  40. ai_review/services/review/internal/inline/types.py +8 -0
  41. ai_review/services/review/internal/inline_reply/schema.py +23 -0
  42. ai_review/services/review/internal/inline_reply/service.py +20 -0
  43. ai_review/services/review/internal/inline_reply/types.py +8 -0
  44. ai_review/services/review/{policy → internal/policy}/service.py +2 -1
  45. ai_review/services/review/internal/policy/types.py +15 -0
  46. ai_review/services/review/{summary → internal/summary}/service.py +2 -2
  47. ai_review/services/review/{summary → internal/summary}/types.py +1 -1
  48. ai_review/services/review/internal/summary_reply/__init__.py +0 -0
  49. ai_review/services/review/internal/summary_reply/schema.py +8 -0
  50. ai_review/services/review/internal/summary_reply/service.py +15 -0
  51. ai_review/services/review/internal/summary_reply/types.py +8 -0
  52. ai_review/services/review/runner/__init__.py +0 -0
  53. ai_review/services/review/runner/context.py +72 -0
  54. ai_review/services/review/runner/inline.py +80 -0
  55. ai_review/services/review/runner/inline_reply.py +80 -0
  56. ai_review/services/review/runner/summary.py +71 -0
  57. ai_review/services/review/runner/summary_reply.py +79 -0
  58. ai_review/services/review/runner/types.py +6 -0
  59. ai_review/services/review/service.py +78 -110
  60. ai_review/services/vcs/bitbucket/adapter.py +27 -0
  61. ai_review/services/vcs/bitbucket/client.py +118 -42
  62. ai_review/services/vcs/github/adapter.py +35 -0
  63. ai_review/services/vcs/github/client.py +105 -44
  64. ai_review/services/vcs/gitlab/adapter.py +28 -0
  65. ai_review/services/vcs/gitlab/client.py +103 -43
  66. ai_review/services/vcs/types.py +34 -0
  67. ai_review/tests/fixtures/clients/bitbucket.py +2 -2
  68. ai_review/tests/fixtures/clients/github.py +35 -6
  69. ai_review/tests/fixtures/clients/gitlab.py +71 -6
  70. ai_review/tests/fixtures/libs/__init__.py +0 -0
  71. ai_review/tests/fixtures/libs/llm/__init__.py +0 -0
  72. ai_review/tests/fixtures/libs/llm/output_json_parser.py +13 -0
  73. ai_review/tests/fixtures/services/hook.py +8 -0
  74. ai_review/tests/fixtures/services/llm.py +8 -5
  75. ai_review/tests/fixtures/services/prompt.py +70 -0
  76. ai_review/tests/fixtures/services/review/base.py +41 -0
  77. ai_review/tests/fixtures/services/review/gateway/__init__.py +0 -0
  78. ai_review/tests/fixtures/services/review/gateway/comment.py +98 -0
  79. ai_review/tests/fixtures/services/review/gateway/llm.py +17 -0
  80. ai_review/tests/fixtures/services/review/internal/__init__.py +0 -0
  81. ai_review/tests/fixtures/services/review/{inline.py → internal/inline.py} +8 -6
  82. ai_review/tests/fixtures/services/review/internal/inline_reply.py +25 -0
  83. ai_review/tests/fixtures/services/review/internal/policy.py +28 -0
  84. ai_review/tests/fixtures/services/review/internal/summary.py +21 -0
  85. ai_review/tests/fixtures/services/review/internal/summary_reply.py +19 -0
  86. ai_review/tests/fixtures/services/review/runner/__init__.py +0 -0
  87. ai_review/tests/fixtures/services/review/runner/context.py +50 -0
  88. ai_review/tests/fixtures/services/review/runner/inline.py +50 -0
  89. ai_review/tests/fixtures/services/review/runner/inline_reply.py +50 -0
  90. ai_review/tests/fixtures/services/review/runner/summary.py +50 -0
  91. ai_review/tests/fixtures/services/review/runner/summary_reply.py +50 -0
  92. ai_review/tests/fixtures/services/vcs.py +23 -0
  93. ai_review/tests/suites/cli/__init__.py +0 -0
  94. ai_review/tests/suites/cli/test_main.py +54 -0
  95. ai_review/tests/suites/libs/config/test_prompt.py +108 -28
  96. ai_review/tests/suites/libs/llm/__init__.py +0 -0
  97. ai_review/tests/suites/libs/llm/test_output_json_parser.py +155 -0
  98. ai_review/tests/suites/services/hook/test_service.py +88 -4
  99. ai_review/tests/suites/services/prompt/test_adapter.py +3 -3
  100. ai_review/tests/suites/services/prompt/test_service.py +102 -58
  101. ai_review/tests/suites/services/prompt/test_tools.py +86 -1
  102. ai_review/tests/suites/services/review/gateway/__init__.py +0 -0
  103. ai_review/tests/suites/services/review/gateway/test_comment.py +253 -0
  104. ai_review/tests/suites/services/review/gateway/test_llm.py +82 -0
  105. ai_review/tests/suites/services/review/internal/__init__.py +0 -0
  106. ai_review/tests/suites/services/review/internal/inline/__init__.py +0 -0
  107. ai_review/tests/suites/services/review/{inline → internal/inline}/test_schema.py +1 -1
  108. ai_review/tests/suites/services/review/internal/inline/test_service.py +81 -0
  109. ai_review/tests/suites/services/review/internal/inline_reply/__init__.py +0 -0
  110. ai_review/tests/suites/services/review/internal/inline_reply/test_schema.py +57 -0
  111. ai_review/tests/suites/services/review/internal/inline_reply/test_service.py +72 -0
  112. ai_review/tests/suites/services/review/internal/policy/__init__.py +0 -0
  113. ai_review/tests/suites/services/review/{policy → internal/policy}/test_service.py +1 -1
  114. ai_review/tests/suites/services/review/internal/summary/__init__.py +0 -0
  115. ai_review/tests/suites/services/review/{summary → internal/summary}/test_schema.py +1 -1
  116. ai_review/tests/suites/services/review/{summary → internal/summary}/test_service.py +2 -2
  117. ai_review/tests/suites/services/review/internal/summary_reply/__init__.py +0 -0
  118. ai_review/tests/suites/services/review/internal/summary_reply/test_schema.py +19 -0
  119. ai_review/tests/suites/services/review/internal/summary_reply/test_service.py +21 -0
  120. ai_review/tests/suites/services/review/runner/__init__.py +0 -0
  121. ai_review/tests/suites/services/review/runner/test_context.py +89 -0
  122. ai_review/tests/suites/services/review/runner/test_inline.py +100 -0
  123. ai_review/tests/suites/services/review/runner/test_inline_reply.py +109 -0
  124. ai_review/tests/suites/services/review/runner/test_summary.py +87 -0
  125. ai_review/tests/suites/services/review/runner/test_summary_reply.py +97 -0
  126. ai_review/tests/suites/services/review/test_service.py +64 -97
  127. ai_review/tests/suites/services/vcs/bitbucket/test_adapter.py +109 -0
  128. ai_review/tests/suites/services/vcs/bitbucket/{test_service.py → test_client.py} +88 -1
  129. ai_review/tests/suites/services/vcs/github/test_adapter.py +162 -0
  130. ai_review/tests/suites/services/vcs/github/{test_service.py → test_client.py} +102 -2
  131. ai_review/tests/suites/services/vcs/gitlab/test_adapter.py +134 -0
  132. ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +113 -3
  133. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/METADATA +8 -5
  134. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/RECORD +146 -72
  135. ai_review/services/review/inline/service.py +0 -54
  136. ai_review/services/review/inline/types.py +0 -11
  137. ai_review/tests/fixtures/services/review/summary.py +0 -19
  138. ai_review/tests/suites/services/review/inline/test_service.py +0 -107
  139. /ai_review/{services/review/inline → libs/llm}/__init__.py +0 -0
  140. /ai_review/services/review/{policy → internal}/__init__.py +0 -0
  141. /ai_review/services/review/{summary → internal/inline}/__init__.py +0 -0
  142. /ai_review/services/review/{inline → internal/inline}/schema.py +0 -0
  143. /ai_review/{tests/suites/services/review/inline → services/review/internal/inline_reply}/__init__.py +0 -0
  144. /ai_review/{tests/suites/services/review → services/review/internal}/policy/__init__.py +0 -0
  145. /ai_review/{tests/suites/services/review → services/review/internal}/summary/__init__.py +0 -0
  146. /ai_review/services/review/{summary → internal/summary}/schema.py +0 -0
  147. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/WHEEL +0 -0
  148. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/entry_points.txt +0 -0
  149. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/licenses/LICENSE +0 -0
  150. {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/top_level.txt +0 -0
@@ -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())
@@ -0,0 +1,6 @@
1
+ from typing import Protocol
2
+
3
+
4
+ class ReviewRunnerProtocol(Protocol):
5
+ async def run(self) -> None:
6
+ ...
@@ -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.policy.service import ReviewPolicyService
15
- from ai_review.services.review.summary.service import SummaryCommentService
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.llm_gateway = ReviewLLMGateway(
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.comment_gateway = ReviewCommentGateway(vcs=self.vcs)
45
+ self.review_comment_gateway = ReviewCommentGateway(vcs=self.vcs)
41
46
 
42
- async def process_file_inline(self, file: str, review_info: ReviewInfoSchema) -> None:
43
- raw_diff = self.git.get_diff_for_file(review_info.base_sha, review_info.head_sha, file)
44
- if not raw_diff.strip():
45
- logger.debug(f"No diff for {file}, skipping")
46
- return
47
-
48
- rendered_file = self.diff.render_file(
49
- file=file,
50
- base_sha=review_info.base_sha,
51
- head_sha=review_info.head_sha,
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
- prompt_context = build_prompt_context_from_mr_info(review_info)
55
- prompt = self.prompt.build_inline_request(rendered_file, prompt_context)
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
- files=changed_files,
99
- base_sha=review_info.base_sha,
100
- head_sha=review_info.head_sha,
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
- comments = self.inline.parse_model_output(prompt_result).dedupe()
108
- comments.root = self.policy.apply_for_context_comments(comments.root)
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
- logger.info(f"Posting {len(comments.root)} inline comments (context review)")
114
- await self.comment_gateway.process_inline_comments(comments)
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 hook.emit_summary_review_start()
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
- summary = self.summary.parse_model_output(prompt_result)
142
- if not summary.text.strip():
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
- logger.info(f"Posting summary review comment ({len(summary.text)} chars)")
147
- await self.comment_gateway.process_summary_comment(summary)
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,27 @@
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
+ file = comment.inline.path if comment.inline and comment.inline.path else None
17
+ line = comment.inline.to_line if comment.inline else None
18
+
19
+ return ReviewCommentSchema(
20
+ id=comment.id,
21
+ body=comment.content.raw or "",
22
+ file=file,
23
+ line=line,
24
+ author=author,
25
+ parent_id=parent_id,
26
+ thread_id=thread_id,
27
+ )