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,13 @@
|
|
|
1
|
+
You are an AI assistant participating in a code review discussion.
|
|
2
|
+
|
|
3
|
+
Your role:
|
|
4
|
+
|
|
5
|
+
- Act as a **technical reviewer**, not the code author.
|
|
6
|
+
- Focus on clarity, correctness, and completeness of the code or proposal.
|
|
7
|
+
- Keep your tone concise, professional, and factual (1–3 sentences).
|
|
8
|
+
- Reply only to the latest message in the discussion thread.
|
|
9
|
+
- When the user requests or implies an action (e.g. adding tests, refactoring, improving performance), provide a
|
|
10
|
+
**specific, actionable suggestion** or short code snippet that addresses it.
|
|
11
|
+
- Avoid greetings, acknowledgements, or filler phrases.
|
|
12
|
+
- Do not summarize past discussion history.
|
|
13
|
+
- If no reply is needed, output exactly: `No reply`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
@@ -7,5 +7,5 @@ class LLMArtifactSchema(BaseModel):
|
|
|
7
7
|
id: str
|
|
8
8
|
prompt: str
|
|
9
9
|
response: str | None = None
|
|
10
|
-
timestamp: str = Field(default_factory=datetime.
|
|
10
|
+
timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
11
11
|
prompt_system: str
|
|
@@ -15,6 +15,12 @@ class HookType(StrEnum):
|
|
|
15
15
|
ON_SUMMARY_REVIEW_START = "ON_SUMMARY_REVIEW_START"
|
|
16
16
|
ON_SUMMARY_REVIEW_COMPLETE = "ON_SUMMARY_REVIEW_COMPLETE"
|
|
17
17
|
|
|
18
|
+
ON_INLINE_REPLY_REVIEW_START = "ON_INLINE_REPLY_REVIEW_START"
|
|
19
|
+
ON_INLINE_REPLY_REVIEW_COMPLETE = "ON_INLINE_REPLY_REVIEW_COMPLETE"
|
|
20
|
+
|
|
21
|
+
ON_SUMMARY_REPLY_REVIEW_START = "ON_SUMMARY_REPLY_REVIEW_START"
|
|
22
|
+
ON_SUMMARY_REPLY_REVIEW_COMPLETE = "ON_SUMMARY_REPLY_REVIEW_COMPLETE"
|
|
23
|
+
|
|
18
24
|
ON_INLINE_COMMENT_START = "ON_INLINE_COMMENT_START"
|
|
19
25
|
ON_INLINE_COMMENT_ERROR = "ON_INLINE_COMMENT_ERROR"
|
|
20
26
|
ON_INLINE_COMMENT_COMPLETE = "ON_INLINE_COMMENT_COMPLETE"
|
|
@@ -22,3 +28,11 @@ class HookType(StrEnum):
|
|
|
22
28
|
ON_SUMMARY_COMMENT_START = "ON_SUMMARY_COMMENT_START"
|
|
23
29
|
ON_SUMMARY_COMMENT_ERROR = "ON_SUMMARY_COMMENT_ERROR"
|
|
24
30
|
ON_SUMMARY_COMMENT_COMPLETE = "ON_SUMMARY_COMMENT_COMPLETE"
|
|
31
|
+
|
|
32
|
+
ON_INLINE_COMMENT_REPLY_START = "ON_INLINE_COMMENT_REPLY_START"
|
|
33
|
+
ON_INLINE_COMMENT_REPLY_ERROR = "ON_INLINE_COMMENT_REPLY_ERROR"
|
|
34
|
+
ON_INLINE_COMMENT_REPLY_COMPLETE = "ON_INLINE_COMMENT_REPLY_COMPLETE"
|
|
35
|
+
|
|
36
|
+
ON_SUMMARY_COMMENT_REPLY_START = "ON_SUMMARY_COMMENT_REPLY_START"
|
|
37
|
+
ON_SUMMARY_COMMENT_REPLY_ERROR = "ON_SUMMARY_COMMENT_REPLY_ERROR"
|
|
38
|
+
ON_SUMMARY_COMMENT_REPLY_COMPLETE = "ON_SUMMARY_COMMENT_REPLY_COMPLETE"
|
|
@@ -19,16 +19,33 @@ from ai_review.services.hook.types import (
|
|
|
19
19
|
# --- Summary Review ---
|
|
20
20
|
SummaryReviewStartHookFunc,
|
|
21
21
|
SummaryReviewCompleteHookFunc,
|
|
22
|
+
# --- Inline Reply Review ---
|
|
23
|
+
InlineReplyReviewStartHookFunc,
|
|
24
|
+
InlineReplyReviewCompleteHookFunc,
|
|
25
|
+
# --- Summary Reply Review ---
|
|
26
|
+
SummaryReplyReviewStartHookFunc,
|
|
27
|
+
SummaryReplyReviewCompleteHookFunc,
|
|
22
28
|
# --- Inline Comment ---
|
|
23
29
|
InlineCommentStartHookFunc,
|
|
24
30
|
InlineCommentErrorHookFunc,
|
|
25
31
|
InlineCommentCompleteHookFunc,
|
|
26
32
|
# --- Summary Comment ---
|
|
27
33
|
SummaryCommentStartHookFunc,
|
|
28
|
-
|
|
34
|
+
SummaryCommentErrorHookFunc,
|
|
35
|
+
SummaryCommentCompleteHookFunc,
|
|
36
|
+
# --- Inline Reply Comment ---
|
|
37
|
+
InlineCommentReplyStartHookFunc,
|
|
38
|
+
InlineCommentReplyErrorHookFunc,
|
|
39
|
+
InlineCommentReplyCompleteHookFunc,
|
|
40
|
+
# --- Summary Reply Comment ---
|
|
41
|
+
SummaryCommentReplyStartHookFunc,
|
|
42
|
+
SummaryCommentReplyErrorHookFunc,
|
|
43
|
+
SummaryCommentReplyCompleteHookFunc
|
|
29
44
|
)
|
|
30
|
-
from ai_review.services.review.inline.schema import InlineCommentSchema
|
|
31
|
-
from ai_review.services.review.
|
|
45
|
+
from ai_review.services.review.internal.inline.schema import InlineCommentSchema
|
|
46
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
47
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
48
|
+
from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
|
|
32
49
|
|
|
33
50
|
logger = get_logger("HOOK_SERVICE")
|
|
34
51
|
|
|
@@ -117,6 +134,36 @@ class HookService:
|
|
|
117
134
|
async def emit_summary_review_complete(self, report: CostReportSchema | None):
|
|
118
135
|
await self.emit(HookType.ON_SUMMARY_REVIEW_COMPLETE, report=report)
|
|
119
136
|
|
|
137
|
+
# --- Inline Reply Review ---
|
|
138
|
+
def on_inline_reply_review_start(self, func: InlineReplyReviewStartHookFunc):
|
|
139
|
+
self.inject_hook(HookType.ON_INLINE_REPLY_REVIEW_START, func)
|
|
140
|
+
return func
|
|
141
|
+
|
|
142
|
+
def on_inline_reply_review_complete(self, func: InlineReplyReviewCompleteHookFunc):
|
|
143
|
+
self.inject_hook(HookType.ON_INLINE_REPLY_REVIEW_COMPLETE, func)
|
|
144
|
+
return func
|
|
145
|
+
|
|
146
|
+
async def emit_inline_reply_review_start(self):
|
|
147
|
+
await self.emit(HookType.ON_INLINE_REPLY_REVIEW_START)
|
|
148
|
+
|
|
149
|
+
async def emit_inline_reply_review_complete(self, report: CostReportSchema | None):
|
|
150
|
+
await self.emit(HookType.ON_INLINE_REPLY_REVIEW_COMPLETE, report=report)
|
|
151
|
+
|
|
152
|
+
# --- Summary Reply Review ---
|
|
153
|
+
def on_summary_reply_review_start(self, func: SummaryReplyReviewStartHookFunc):
|
|
154
|
+
self.inject_hook(HookType.ON_SUMMARY_REPLY_REVIEW_START, func)
|
|
155
|
+
return func
|
|
156
|
+
|
|
157
|
+
def on_summary_reply_review_complete(self, func: SummaryReplyReviewCompleteHookFunc):
|
|
158
|
+
self.inject_hook(HookType.ON_SUMMARY_REPLY_REVIEW_COMPLETE, func)
|
|
159
|
+
return func
|
|
160
|
+
|
|
161
|
+
async def emit_summary_reply_review_start(self):
|
|
162
|
+
await self.emit(HookType.ON_SUMMARY_REPLY_REVIEW_START)
|
|
163
|
+
|
|
164
|
+
async def emit_summary_reply_review_complete(self, report: CostReportSchema | None):
|
|
165
|
+
await self.emit(HookType.ON_SUMMARY_REPLY_REVIEW_COMPLETE, report=report)
|
|
166
|
+
|
|
120
167
|
# --- Inline Comment ---
|
|
121
168
|
def on_inline_comment_start(self, func: InlineCommentStartHookFunc):
|
|
122
169
|
self.inject_hook(HookType.ON_INLINE_COMMENT_START, func)
|
|
@@ -144,7 +191,7 @@ class HookService:
|
|
|
144
191
|
self.inject_hook(HookType.ON_SUMMARY_COMMENT_START, func)
|
|
145
192
|
return func
|
|
146
193
|
|
|
147
|
-
def on_summary_comment_error(self, func:
|
|
194
|
+
def on_summary_comment_error(self, func: SummaryCommentErrorHookFunc):
|
|
148
195
|
self.inject_hook(HookType.ON_SUMMARY_COMMENT_ERROR, func)
|
|
149
196
|
return func
|
|
150
197
|
|
|
@@ -160,3 +207,47 @@ class HookService:
|
|
|
160
207
|
|
|
161
208
|
async def emit_summary_comment_complete(self, comment: SummaryCommentSchema):
|
|
162
209
|
await self.emit(HookType.ON_SUMMARY_COMMENT_COMPLETE, comment=comment)
|
|
210
|
+
|
|
211
|
+
# --- Inline Reply Comment ---
|
|
212
|
+
def on_inline_comment_reply_start(self, func: InlineCommentReplyStartHookFunc):
|
|
213
|
+
self.inject_hook(HookType.ON_INLINE_COMMENT_REPLY_START, func)
|
|
214
|
+
return func
|
|
215
|
+
|
|
216
|
+
def on_inline_comment_reply_error(self, func: InlineCommentReplyErrorHookFunc):
|
|
217
|
+
self.inject_hook(HookType.ON_INLINE_COMMENT_REPLY_ERROR, func)
|
|
218
|
+
return func
|
|
219
|
+
|
|
220
|
+
def on_inline_comment_reply_complete(self, func: InlineCommentReplyCompleteHookFunc):
|
|
221
|
+
self.inject_hook(HookType.ON_INLINE_COMMENT_REPLY_COMPLETE, func)
|
|
222
|
+
return func
|
|
223
|
+
|
|
224
|
+
async def emit_inline_comment_reply_start(self, comment: InlineCommentReplySchema):
|
|
225
|
+
await self.emit(HookType.ON_INLINE_COMMENT_REPLY_START, comment=comment)
|
|
226
|
+
|
|
227
|
+
async def emit_inline_comment_reply_error(self, comment: InlineCommentReplySchema):
|
|
228
|
+
await self.emit(HookType.ON_INLINE_COMMENT_REPLY_ERROR, comment=comment)
|
|
229
|
+
|
|
230
|
+
async def emit_inline_comment_reply_complete(self, comment: InlineCommentReplySchema):
|
|
231
|
+
await self.emit(HookType.ON_INLINE_COMMENT_REPLY_COMPLETE, comment=comment)
|
|
232
|
+
|
|
233
|
+
# --- Inline Reply Comment ---
|
|
234
|
+
def on_summary_comment_reply_start(self, func: SummaryCommentReplyStartHookFunc):
|
|
235
|
+
self.inject_hook(HookType.ON_SUMMARY_COMMENT_REPLY_START, func)
|
|
236
|
+
return func
|
|
237
|
+
|
|
238
|
+
def on_summary_comment_reply_error(self, func: SummaryCommentReplyErrorHookFunc):
|
|
239
|
+
self.inject_hook(HookType.ON_SUMMARY_COMMENT_REPLY_ERROR, func)
|
|
240
|
+
return func
|
|
241
|
+
|
|
242
|
+
def on_summary_comment_reply_complete(self, func: SummaryCommentReplyCompleteHookFunc):
|
|
243
|
+
self.inject_hook(HookType.ON_SUMMARY_COMMENT_REPLY_COMPLETE, func)
|
|
244
|
+
return func
|
|
245
|
+
|
|
246
|
+
async def emit_summary_comment_reply_start(self, comment: SummaryCommentReplySchema):
|
|
247
|
+
await self.emit(HookType.ON_SUMMARY_COMMENT_REPLY_START, comment=comment)
|
|
248
|
+
|
|
249
|
+
async def emit_summary_comment_reply_error(self, comment: SummaryCommentReplySchema):
|
|
250
|
+
await self.emit(HookType.ON_SUMMARY_COMMENT_REPLY_ERROR, comment=comment)
|
|
251
|
+
|
|
252
|
+
async def emit_summary_comment_reply_complete(self, comment: SummaryCommentReplySchema):
|
|
253
|
+
await self.emit(HookType.ON_SUMMARY_COMMENT_REPLY_COMPLETE, comment=comment)
|
ai_review/services/hook/types.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import Callable, Awaitable
|
|
2
2
|
|
|
3
3
|
from ai_review.services.cost.schema import CostReportSchema
|
|
4
|
-
from ai_review.services.review.inline.schema import InlineCommentSchema
|
|
5
|
-
from ai_review.services.review.
|
|
4
|
+
from ai_review.services.review.internal.inline.schema import InlineCommentSchema
|
|
5
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
6
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
7
|
+
from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
|
|
6
8
|
|
|
7
9
|
HookFunc = Callable[..., Awaitable[None]]
|
|
8
10
|
|
|
@@ -19,6 +21,12 @@ ContextReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[No
|
|
|
19
21
|
SummaryReviewStartHookFunc = Callable[..., Awaitable[None]]
|
|
20
22
|
SummaryReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[None]]
|
|
21
23
|
|
|
24
|
+
InlineReplyReviewStartHookFunc = Callable[..., Awaitable[None]]
|
|
25
|
+
InlineReplyReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[None]]
|
|
26
|
+
|
|
27
|
+
SummaryReplyReviewStartHookFunc = Callable[..., Awaitable[None]]
|
|
28
|
+
SummaryReplyReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[None]]
|
|
29
|
+
|
|
22
30
|
InlineCommentStartHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
|
|
23
31
|
InlineCommentErrorHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
|
|
24
32
|
InlineCommentCompleteHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
|
|
@@ -26,3 +34,11 @@ InlineCommentCompleteHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
|
|
|
26
34
|
SummaryCommentStartHookFunc = Callable[[SummaryCommentSchema], Awaitable[None]]
|
|
27
35
|
SummaryCommentErrorHookFunc = Callable[[SummaryCommentSchema], Awaitable[None]]
|
|
28
36
|
SummaryCommentCompleteHookFunc = Callable[[SummaryCommentSchema], Awaitable[None]]
|
|
37
|
+
|
|
38
|
+
InlineCommentReplyStartHookFunc = Callable[[InlineCommentReplySchema], Awaitable[None]]
|
|
39
|
+
InlineCommentReplyErrorHookFunc = Callable[[InlineCommentReplySchema], Awaitable[None]]
|
|
40
|
+
InlineCommentReplyCompleteHookFunc = Callable[[InlineCommentReplySchema], Awaitable[None]]
|
|
41
|
+
|
|
42
|
+
SummaryCommentReplyStartHookFunc = Callable[[SummaryCommentReplySchema], Awaitable[None]]
|
|
43
|
+
SummaryCommentReplyErrorHookFunc = Callable[[SummaryCommentReplySchema], Awaitable[None]]
|
|
44
|
+
SummaryCommentReplyCompleteHookFunc = Callable[[SummaryCommentReplySchema], Awaitable[None]]
|
|
@@ -2,7 +2,7 @@ from ai_review.services.prompt.schema import PromptContextSchema
|
|
|
2
2
|
from ai_review.services.vcs.types import ReviewInfoSchema
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def
|
|
5
|
+
def build_prompt_context_from_review_info(review: ReviewInfoSchema) -> PromptContextSchema:
|
|
6
6
|
return PromptContextSchema(
|
|
7
7
|
review_title=review.title,
|
|
8
8
|
review_description=review.description,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from ai_review.config import settings
|
|
2
2
|
from ai_review.services.diff.schema import DiffFileSchema
|
|
3
3
|
from ai_review.services.prompt.schema import PromptContextSchema
|
|
4
|
-
from ai_review.services.prompt.tools import normalize_prompt, format_file
|
|
4
|
+
from ai_review.services.prompt.tools import normalize_prompt, format_file, format_thread, format_files
|
|
5
5
|
from ai_review.services.prompt.types import PromptServiceProtocol
|
|
6
|
+
from ai_review.services.vcs.types import ReviewThreadSchema
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class PromptService(PromptServiceProtocol):
|
|
@@ -28,7 +29,7 @@ class PromptService(PromptServiceProtocol):
|
|
|
28
29
|
@classmethod
|
|
29
30
|
def build_summary_request(cls, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
|
|
30
31
|
prompt = cls.prepare_prompt(settings.prompt.load_summary(), context)
|
|
31
|
-
changes =
|
|
32
|
+
changes = format_files(diffs)
|
|
32
33
|
return (
|
|
33
34
|
f"{prompt}\n\n"
|
|
34
35
|
f"## Changes\n\n"
|
|
@@ -38,13 +39,50 @@ class PromptService(PromptServiceProtocol):
|
|
|
38
39
|
@classmethod
|
|
39
40
|
def build_context_request(cls, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
|
|
40
41
|
prompt = cls.prepare_prompt(settings.prompt.load_context(), context)
|
|
41
|
-
changes =
|
|
42
|
+
changes = format_files(diffs)
|
|
42
43
|
return (
|
|
43
44
|
f"{prompt}\n\n"
|
|
44
45
|
f"## Diff\n\n"
|
|
45
46
|
f"{changes}\n"
|
|
46
47
|
)
|
|
47
48
|
|
|
49
|
+
@classmethod
|
|
50
|
+
def build_inline_reply_request(
|
|
51
|
+
cls,
|
|
52
|
+
diff: DiffFileSchema,
|
|
53
|
+
thread: ReviewThreadSchema,
|
|
54
|
+
context: PromptContextSchema
|
|
55
|
+
) -> str:
|
|
56
|
+
prompt = cls.prepare_prompt(settings.prompt.load_inline_reply(), context)
|
|
57
|
+
conversation = format_thread(thread)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
f"{prompt}\n\n"
|
|
61
|
+
f"## Conversation\n\n"
|
|
62
|
+
f"{conversation}\n\n"
|
|
63
|
+
f"## Diff\n\n"
|
|
64
|
+
f"{format_file(diff)}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def build_summary_reply_request(
|
|
69
|
+
cls,
|
|
70
|
+
diffs: list[DiffFileSchema],
|
|
71
|
+
thread: ReviewThreadSchema,
|
|
72
|
+
context: PromptContextSchema
|
|
73
|
+
) -> str:
|
|
74
|
+
prompt = cls.prepare_prompt(settings.prompt.load_summary_reply(), context)
|
|
75
|
+
changes = format_files(diffs)
|
|
76
|
+
conversation = format_thread(thread)
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
f"{prompt}\n\n"
|
|
80
|
+
f"## Conversation\n\n"
|
|
81
|
+
f"{conversation}\n\n"
|
|
82
|
+
f"## Changes\n\n"
|
|
83
|
+
f"{changes}"
|
|
84
|
+
)
|
|
85
|
+
|
|
48
86
|
@classmethod
|
|
49
87
|
def build_system_inline_request(cls, context: PromptContextSchema) -> str:
|
|
50
88
|
return cls.prepare_prompt(settings.prompt.load_system_inline(), context)
|
|
@@ -56,3 +94,11 @@ class PromptService(PromptServiceProtocol):
|
|
|
56
94
|
@classmethod
|
|
57
95
|
def build_system_summary_request(cls, context: PromptContextSchema) -> str:
|
|
58
96
|
return cls.prepare_prompt(settings.prompt.load_system_summary(), context)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def build_system_inline_reply_request(cls, context: PromptContextSchema) -> str:
|
|
100
|
+
return cls.prepare_prompt(settings.prompt.load_system_inline_reply(), context)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def build_system_summary_reply_request(cls, context: PromptContextSchema) -> str:
|
|
104
|
+
return cls.prepare_prompt(settings.prompt.load_system_summary_reply(), context)
|
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
|
|
3
3
|
from ai_review.libs.logger import get_logger
|
|
4
4
|
from ai_review.services.diff.schema import DiffFileSchema
|
|
5
|
+
from ai_review.services.vcs.types import ReviewThreadSchema
|
|
5
6
|
|
|
6
7
|
logger = get_logger("PROMPT_TOOLS")
|
|
7
8
|
|
|
@@ -10,6 +11,26 @@ def format_file(diff: DiffFileSchema) -> str:
|
|
|
10
11
|
return f"# File: {diff.file}\n{diff.diff}\n"
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def format_files(diffs: list[DiffFileSchema]) -> str:
|
|
15
|
+
return "\n\n".join(map(format_file, diffs))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_thread(thread: ReviewThreadSchema) -> str:
|
|
19
|
+
if not thread.comments:
|
|
20
|
+
return "No comments in thread."
|
|
21
|
+
|
|
22
|
+
lines: list[str] = []
|
|
23
|
+
for comment in thread.comments:
|
|
24
|
+
user = (comment.author.name or comment.author.username or "User").strip()
|
|
25
|
+
body = (comment.body or "").strip()
|
|
26
|
+
if not body:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
lines.append(f"- {user}: {body}")
|
|
30
|
+
|
|
31
|
+
return "\n\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
13
34
|
def normalize_prompt(text: str) -> str:
|
|
14
35
|
tails_stripped = [re.sub(r"[ \t]+$", "", line) for line in text.splitlines()]
|
|
15
36
|
text = "\n".join(tails_stripped)
|
|
@@ -2,6 +2,7 @@ from typing import Protocol
|
|
|
2
2
|
|
|
3
3
|
from ai_review.services.diff.schema import DiffFileSchema
|
|
4
4
|
from ai_review.services.prompt.schema import PromptContextSchema
|
|
5
|
+
from ai_review.services.vcs.types import ReviewThreadSchema
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class PromptServiceProtocol(Protocol):
|
|
@@ -17,6 +18,22 @@ class PromptServiceProtocol(Protocol):
|
|
|
17
18
|
def build_context_request(self, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
|
|
18
19
|
...
|
|
19
20
|
|
|
21
|
+
def build_inline_reply_request(
|
|
22
|
+
self,
|
|
23
|
+
diff: DiffFileSchema,
|
|
24
|
+
thread: ReviewThreadSchema,
|
|
25
|
+
context: PromptContextSchema
|
|
26
|
+
) -> str:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def build_summary_reply_request(
|
|
30
|
+
self,
|
|
31
|
+
diffs: list[DiffFileSchema],
|
|
32
|
+
thread: ReviewThreadSchema,
|
|
33
|
+
context: PromptContextSchema
|
|
34
|
+
) -> str:
|
|
35
|
+
...
|
|
36
|
+
|
|
20
37
|
def build_system_inline_request(self, context: PromptContextSchema) -> str:
|
|
21
38
|
...
|
|
22
39
|
|
|
@@ -25,3 +42,9 @@ class PromptServiceProtocol(Protocol):
|
|
|
25
42
|
|
|
26
43
|
def build_system_summary_request(self, context: PromptContextSchema) -> str:
|
|
27
44
|
...
|
|
45
|
+
|
|
46
|
+
def build_system_inline_reply_request(self, context: PromptContextSchema) -> str:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def build_system_summary_reply_request(self, context: PromptContextSchema) -> str:
|
|
50
|
+
...
|
|
@@ -2,17 +2,38 @@ from ai_review.config import settings
|
|
|
2
2
|
from ai_review.libs.asynchronous.gather import bounded_gather
|
|
3
3
|
from ai_review.libs.logger import get_logger
|
|
4
4
|
from ai_review.services.hook import hook
|
|
5
|
-
from ai_review.services.review.
|
|
6
|
-
from ai_review.services.review.
|
|
7
|
-
from ai_review.services.
|
|
5
|
+
from ai_review.services.review.gateway.types import ReviewCommentGatewayProtocol
|
|
6
|
+
from ai_review.services.review.internal.inline.schema import InlineCommentListSchema, InlineCommentSchema
|
|
7
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
8
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
9
|
+
from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
|
|
10
|
+
from ai_review.services.vcs.types import VCSClientProtocol, ReviewThreadSchema
|
|
8
11
|
|
|
9
12
|
logger = get_logger("REVIEW_COMMENT_GATEWAY")
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
class ReviewCommentGateway:
|
|
15
|
+
class ReviewCommentGateway(ReviewCommentGatewayProtocol):
|
|
13
16
|
def __init__(self, vcs: VCSClientProtocol):
|
|
14
17
|
self.vcs = vcs
|
|
15
18
|
|
|
19
|
+
async def get_inline_threads(self) -> list[ReviewThreadSchema]:
|
|
20
|
+
threads = await self.vcs.get_inline_threads()
|
|
21
|
+
inline_threads = [
|
|
22
|
+
thread for thread in threads
|
|
23
|
+
if any(settings.review.inline_reply_tag in comment.body for comment in thread.comments)
|
|
24
|
+
]
|
|
25
|
+
logger.info(f"Detected {len(inline_threads)}/{len(threads)} AI inline threads")
|
|
26
|
+
return inline_threads
|
|
27
|
+
|
|
28
|
+
async def get_summary_threads(self) -> list[ReviewThreadSchema]:
|
|
29
|
+
threads = await self.vcs.get_general_threads()
|
|
30
|
+
summary_threads = [
|
|
31
|
+
thread for thread in threads
|
|
32
|
+
if any(settings.review.summary_reply_tag in comment.body for comment in thread.comments)
|
|
33
|
+
]
|
|
34
|
+
logger.info(f"Detected {len(summary_threads)}/{len(threads)} AI summary threads")
|
|
35
|
+
return summary_threads
|
|
36
|
+
|
|
16
37
|
async def has_existing_inline_comments(self) -> bool:
|
|
17
38
|
comments = await self.vcs.get_inline_comments()
|
|
18
39
|
has_comments = any(
|
|
@@ -34,7 +55,25 @@ class ReviewCommentGateway:
|
|
|
34
55
|
|
|
35
56
|
return has_comments
|
|
36
57
|
|
|
37
|
-
async def
|
|
58
|
+
async def process_inline_reply(self, thread_id: str, reply: InlineCommentReplySchema) -> None:
|
|
59
|
+
try:
|
|
60
|
+
await hook.emit_inline_comment_reply_start(reply)
|
|
61
|
+
await self.vcs.create_inline_reply(thread_id, reply.body_with_tag)
|
|
62
|
+
await hook.emit_inline_comment_reply_complete(reply)
|
|
63
|
+
except Exception as error:
|
|
64
|
+
logger.exception(f"Failed to create inline reply for thread {thread_id}: {error}")
|
|
65
|
+
await hook.emit_inline_comment_reply_error(reply)
|
|
66
|
+
|
|
67
|
+
async def process_summary_reply(self, thread_id: str, reply: SummaryCommentReplySchema) -> None:
|
|
68
|
+
try:
|
|
69
|
+
await hook.emit_summary_comment_reply_start(reply)
|
|
70
|
+
await self.vcs.create_summary_reply(thread_id, reply.body_with_tag)
|
|
71
|
+
await hook.emit_summary_comment_reply_complete(reply)
|
|
72
|
+
except Exception as error:
|
|
73
|
+
logger.exception(f"Failed to create summary reply for thread {thread_id}: {error}")
|
|
74
|
+
await hook.emit_summary_comment_reply_error(reply)
|
|
75
|
+
|
|
76
|
+
async def process_inline_comment(self, comment: InlineCommentSchema) -> None:
|
|
38
77
|
try:
|
|
39
78
|
await hook.emit_inline_comment_start(comment)
|
|
40
79
|
await self.vcs.create_inline_comment(
|
|
@@ -52,7 +91,7 @@ class ReviewCommentGateway:
|
|
|
52
91
|
logger.warning(f"Falling back to general comment for {comment.file}:{comment.line}")
|
|
53
92
|
await self.process_summary_comment(SummaryCommentSchema(text=comment.fallback_body))
|
|
54
93
|
|
|
55
|
-
async def process_summary_comment(self, comment: SummaryCommentSchema):
|
|
94
|
+
async def process_summary_comment(self, comment: SummaryCommentSchema) -> None:
|
|
56
95
|
try:
|
|
57
96
|
await hook.emit_summary_comment_start(comment)
|
|
58
97
|
await self.vcs.create_general_comment(comment.body_with_tag)
|
|
@@ -3,11 +3,12 @@ from ai_review.services.artifacts.types import ArtifactsServiceProtocol
|
|
|
3
3
|
from ai_review.services.cost.types import CostServiceProtocol
|
|
4
4
|
from ai_review.services.hook import hook
|
|
5
5
|
from ai_review.services.llm.types import LLMClientProtocol
|
|
6
|
+
from ai_review.services.review.gateway.types import ReviewLLMGatewayProtocol
|
|
6
7
|
|
|
7
8
|
logger = get_logger("REVIEW_LLM_GATEWAY")
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class ReviewLLMGateway:
|
|
11
|
+
class ReviewLLMGateway(ReviewLLMGatewayProtocol):
|
|
11
12
|
def __init__(
|
|
12
13
|
self,
|
|
13
14
|
llm: LLMClientProtocol,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.services.artifacts.types import ArtifactsServiceProtocol
|
|
4
|
+
from ai_review.services.cost.types import CostServiceProtocol
|
|
5
|
+
from ai_review.services.llm.types import LLMClientProtocol
|
|
6
|
+
from ai_review.services.review.internal.inline.schema import InlineCommentSchema, InlineCommentListSchema
|
|
7
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
8
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
9
|
+
from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
|
|
10
|
+
from ai_review.services.vcs.types import VCSClientProtocol, ReviewThreadSchema
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReviewLLMGatewayProtocol(Protocol):
|
|
14
|
+
llm: LLMClientProtocol
|
|
15
|
+
cost: CostServiceProtocol
|
|
16
|
+
artifacts: ArtifactsServiceProtocol
|
|
17
|
+
|
|
18
|
+
async def ask(self, prompt: str, prompt_system: str) -> str:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReviewCommentGatewayProtocol(Protocol):
|
|
23
|
+
vcs: VCSClientProtocol
|
|
24
|
+
|
|
25
|
+
async def get_inline_threads(self) -> list[ReviewThreadSchema]:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
async def get_summary_threads(self) -> list[ReviewThreadSchema]:
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
async def has_existing_inline_comments(self) -> bool:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
async def has_existing_summary_comments(self) -> bool:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
async def process_inline_reply(self, thread_id: str, reply: InlineCommentReplySchema) -> None:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
async def process_summary_reply(self, thread_id: str, reply: SummaryCommentReplySchema) -> None:
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
async def process_inline_comment(self, comment: InlineCommentSchema) -> None:
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
async def process_summary_comment(self, comment: SummaryCommentSchema) -> None:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def process_inline_comments(self, comments: InlineCommentListSchema) -> None:
|
|
50
|
+
...
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.llm.output_json_parser import LLMOutputJSONParser
|
|
4
|
+
from ai_review.libs.logger import get_logger
|
|
5
|
+
from ai_review.services.review.internal.inline.schema import InlineCommentListSchema
|
|
6
|
+
from ai_review.services.review.internal.inline.types import InlineCommentServiceProtocol
|
|
7
|
+
|
|
8
|
+
logger = get_logger("INLINE_COMMENT_SERVICE")
|
|
9
|
+
|
|
10
|
+
FIRST_JSON_ARRAY_RE = re.compile(r"\[[\s\S]*]", re.MULTILINE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InlineCommentService(InlineCommentServiceProtocol):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.parser = LLMOutputJSONParser(model=InlineCommentListSchema)
|
|
16
|
+
|
|
17
|
+
def parse_model_output(self, output: str) -> InlineCommentListSchema:
|
|
18
|
+
output = (output or "").strip()
|
|
19
|
+
if not output:
|
|
20
|
+
logger.warning("LLM returned empty string for inline review")
|
|
21
|
+
return InlineCommentListSchema(root=[])
|
|
22
|
+
|
|
23
|
+
if parsed := self.parser.parse_output(output):
|
|
24
|
+
return parsed
|
|
25
|
+
|
|
26
|
+
logger.warning("Failed to parse JSON, trying to extract first JSON array...")
|
|
27
|
+
|
|
28
|
+
if json_array_match := FIRST_JSON_ARRAY_RE.search(output):
|
|
29
|
+
extracted = json_array_match.group(0)
|
|
30
|
+
logger.debug(f"Extracted potential JSON array (len={len(extracted)})")
|
|
31
|
+
|
|
32
|
+
if parsed := self.parser.try_parse(extracted):
|
|
33
|
+
logger.info("Successfully parsed JSON after extracting array from output")
|
|
34
|
+
return parsed
|
|
35
|
+
else:
|
|
36
|
+
logger.error("Extracted JSON array is still invalid after sanitization")
|
|
37
|
+
else:
|
|
38
|
+
logger.error("No JSON array found in LLM output")
|
|
39
|
+
|
|
40
|
+
return InlineCommentListSchema(root=[])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, field_validator
|
|
2
|
+
|
|
3
|
+
from ai_review.config import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InlineCommentReplySchema(BaseModel):
|
|
7
|
+
message: str = Field(min_length=1)
|
|
8
|
+
suggestion: str | None = None
|
|
9
|
+
|
|
10
|
+
@field_validator("message")
|
|
11
|
+
def normalize_message(cls, value: str) -> str:
|
|
12
|
+
return value.strip()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def body(self) -> str:
|
|
16
|
+
if self.suggestion:
|
|
17
|
+
return f"{self.message}\n\n```suggestion\n{self.suggestion}\n```"
|
|
18
|
+
|
|
19
|
+
return self.message
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def body_with_tag(self) -> str:
|
|
23
|
+
return f"{self.body}\n\n{settings.review.inline_reply_tag}"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ai_review.libs.llm.output_json_parser import LLMOutputJSONParser
|
|
2
|
+
from ai_review.libs.logger import get_logger
|
|
3
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
4
|
+
from ai_review.services.review.internal.inline_reply.types import InlineCommentReplyServiceProtocol
|
|
5
|
+
|
|
6
|
+
logger = get_logger("INLINE_COMMENT_REPLY_SERVICE")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InlineCommentReplyService(InlineCommentReplyServiceProtocol):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.parser = LLMOutputJSONParser(model=InlineCommentReplySchema)
|
|
12
|
+
|
|
13
|
+
def parse_model_output(self, output: str) -> InlineCommentReplySchema | None:
|
|
14
|
+
logger.debug("Parsing LLM output for inline reply...")
|
|
15
|
+
parsed = self.parser.parse_output(output)
|
|
16
|
+
if parsed:
|
|
17
|
+
logger.debug("Inline reply parsed successfully")
|
|
18
|
+
else:
|
|
19
|
+
logger.warning("Inline reply parse failed or model returned empty JSON")
|
|
20
|
+
return parsed
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InlineCommentReplyServiceProtocol(Protocol):
|
|
7
|
+
def parse_model_output(self, output: str) -> InlineCommentReplySchema | None:
|
|
8
|
+
...
|
|
@@ -2,11 +2,12 @@ import fnmatch
|
|
|
2
2
|
|
|
3
3
|
from ai_review.config import settings
|
|
4
4
|
from ai_review.libs.logger import get_logger
|
|
5
|
+
from ai_review.services.review.internal.policy.types import ReviewPolicyServiceProtocol
|
|
5
6
|
|
|
6
7
|
logger = get_logger("REVIEW_POLICY_SERVICE")
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class ReviewPolicyService:
|
|
10
|
+
class ReviewPolicyService(ReviewPolicyServiceProtocol):
|
|
10
11
|
@classmethod
|
|
11
12
|
def should_review_file(cls, file: str) -> bool:
|
|
12
13
|
review = settings.review
|