xai-review 0.20.0__py3-none-any.whl → 0.22.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/clients/claude/client.py +1 -1
- ai_review/clients/gemini/client.py +1 -1
- ai_review/clients/github/client.py +1 -1
- ai_review/clients/github/pr/client.py +64 -16
- ai_review/clients/github/pr/schema/comments.py +4 -0
- ai_review/clients/github/pr/schema/files.py +4 -0
- ai_review/clients/github/pr/schema/reviews.py +4 -0
- ai_review/clients/github/pr/types.py +49 -0
- ai_review/clients/gitlab/client.py +1 -1
- ai_review/clients/gitlab/mr/client.py +25 -8
- ai_review/clients/gitlab/mr/schema/discussions.py +4 -0
- ai_review/clients/gitlab/mr/schema/notes.py +4 -0
- ai_review/clients/gitlab/mr/types.py +35 -0
- ai_review/clients/openai/client.py +1 -1
- ai_review/config.py +2 -0
- ai_review/libs/asynchronous/gather.py +6 -3
- ai_review/libs/config/core.py +5 -0
- ai_review/libs/http/event_hooks/logger.py +5 -2
- ai_review/libs/http/transports/retry.py +23 -6
- ai_review/services/artifacts/service.py +2 -1
- ai_review/services/artifacts/types.py +20 -0
- ai_review/services/cost/service.py +2 -1
- ai_review/services/cost/types.py +12 -0
- ai_review/services/diff/service.py +2 -1
- ai_review/services/diff/types.py +28 -0
- ai_review/services/hook/__init__.py +5 -0
- ai_review/services/hook/constants.py +24 -0
- ai_review/services/hook/service.py +162 -0
- ai_review/services/hook/types.py +28 -0
- ai_review/services/llm/claude/client.py +2 -2
- ai_review/services/llm/factory.py +2 -2
- ai_review/services/llm/gemini/client.py +2 -2
- ai_review/services/llm/openai/client.py +2 -2
- ai_review/services/llm/types.py +1 -1
- ai_review/services/prompt/service.py +2 -1
- ai_review/services/prompt/types.py +27 -0
- ai_review/services/review/gateway/__init__.py +0 -0
- ai_review/services/review/gateway/comment.py +65 -0
- ai_review/services/review/gateway/llm.py +40 -0
- ai_review/services/review/inline/schema.py +2 -2
- ai_review/services/review/inline/service.py +2 -1
- ai_review/services/review/inline/types.py +11 -0
- ai_review/services/review/service.py +23 -74
- ai_review/services/review/summary/service.py +2 -1
- ai_review/services/review/summary/types.py +8 -0
- ai_review/services/vcs/factory.py +2 -2
- ai_review/services/vcs/github/client.py +4 -2
- ai_review/services/vcs/gitlab/client.py +4 -2
- ai_review/services/vcs/types.py +1 -1
- ai_review/tests/fixtures/clients/__init__.py +0 -0
- ai_review/tests/fixtures/clients/claude.py +22 -0
- ai_review/tests/fixtures/clients/gemini.py +21 -0
- ai_review/tests/fixtures/clients/github.py +181 -0
- ai_review/tests/fixtures/clients/gitlab.py +150 -0
- ai_review/tests/fixtures/clients/openai.py +21 -0
- ai_review/tests/fixtures/services/__init__.py +0 -0
- ai_review/tests/fixtures/services/artifacts.py +51 -0
- ai_review/tests/fixtures/services/cost.py +48 -0
- ai_review/tests/fixtures/services/diff.py +46 -0
- ai_review/tests/fixtures/{git.py → services/git.py} +11 -5
- ai_review/tests/fixtures/services/llm.py +26 -0
- ai_review/tests/fixtures/services/prompt.py +43 -0
- ai_review/tests/fixtures/services/review/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/inline.py +25 -0
- ai_review/tests/fixtures/services/review/summary.py +19 -0
- ai_review/tests/fixtures/services/vcs.py +49 -0
- ai_review/tests/suites/clients/claude/test_client.py +1 -20
- ai_review/tests/suites/clients/gemini/test_client.py +1 -19
- ai_review/tests/suites/clients/github/test_client.py +1 -23
- ai_review/tests/suites/clients/gitlab/test_client.py +1 -22
- ai_review/tests/suites/clients/openai/test_client.py +1 -19
- ai_review/tests/suites/libs/asynchronous/__init__.py +0 -0
- ai_review/tests/suites/libs/asynchronous/test_gather.py +46 -0
- ai_review/tests/suites/services/diff/test_service.py +4 -4
- ai_review/tests/suites/services/diff/test_tools.py +10 -10
- ai_review/tests/suites/services/hook/__init__.py +0 -0
- ai_review/tests/suites/services/hook/test_service.py +93 -0
- ai_review/tests/suites/services/llm/__init__.py +0 -0
- ai_review/tests/suites/services/llm/test_factory.py +30 -0
- ai_review/tests/suites/services/review/inline/test_schema.py +10 -9
- ai_review/tests/suites/services/review/summary/test_schema.py +0 -1
- ai_review/tests/suites/services/review/summary/test_service.py +10 -7
- ai_review/tests/suites/services/review/test_service.py +126 -0
- ai_review/tests/suites/services/vcs/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/github/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/github/test_service.py +114 -0
- ai_review/tests/suites/services/vcs/gitlab/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/gitlab/test_service.py +123 -0
- ai_review/tests/suites/services/vcs/test_factory.py +23 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/METADATA +5 -2
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/RECORD +95 -50
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/WHEEL +0 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -26,7 +26,7 @@ class ClaudeHTTPClient(HTTPClient):
|
|
|
26
26
|
def get_claude_http_client() -> ClaudeHTTPClient:
|
|
27
27
|
logger = get_logger("CLAUDE_HTTP_CLIENT")
|
|
28
28
|
logger_event_hook = LoggerEventHook(logger=logger)
|
|
29
|
-
retry_transport = RetryTransport(transport=AsyncHTTPTransport())
|
|
29
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
30
30
|
|
|
31
31
|
client = AsyncClient(
|
|
32
32
|
timeout=settings.llm.http_client.timeout,
|
|
@@ -29,7 +29,7 @@ class GeminiHTTPClient(HTTPClient):
|
|
|
29
29
|
def get_gemini_http_client() -> GeminiHTTPClient:
|
|
30
30
|
logger = get_logger("GEMINI_HTTP_CLIENT")
|
|
31
31
|
logger_event_hook = LoggerEventHook(logger=logger)
|
|
32
|
-
retry_transport = RetryTransport(transport=AsyncHTTPTransport())
|
|
32
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
33
33
|
|
|
34
34
|
client = AsyncClient(
|
|
35
35
|
timeout=settings.llm.http_client.timeout,
|
|
@@ -15,7 +15,7 @@ class GitHubHTTPClient:
|
|
|
15
15
|
def get_github_http_client() -> GitHubHTTPClient:
|
|
16
16
|
logger = get_logger("GITHUB_HTTP_CLIENT")
|
|
17
17
|
logger_event_hook = LoggerEventHook(logger=logger)
|
|
18
|
-
retry_transport = RetryTransport(transport=AsyncHTTPTransport())
|
|
18
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
19
19
|
|
|
20
20
|
client = AsyncClient(
|
|
21
21
|
timeout=settings.llm.http_client.timeout,
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
from httpx import Response
|
|
1
|
+
from httpx import Response, QueryParams
|
|
2
2
|
|
|
3
3
|
from ai_review.clients.github.pr.schema.comments import (
|
|
4
|
+
GitHubGetPRCommentsQuerySchema,
|
|
4
5
|
GitHubGetPRCommentsResponseSchema,
|
|
5
6
|
GitHubCreateIssueCommentRequestSchema,
|
|
6
7
|
GitHubCreateIssueCommentResponseSchema,
|
|
7
8
|
GitHubCreateReviewCommentRequestSchema,
|
|
8
9
|
GitHubCreateReviewCommentResponseSchema
|
|
9
10
|
)
|
|
10
|
-
from ai_review.clients.github.pr.schema.files import
|
|
11
|
+
from ai_review.clients.github.pr.schema.files import (
|
|
12
|
+
GitHubGetPRFilesQuerySchema,
|
|
13
|
+
GitHubGetPRFilesResponseSchema
|
|
14
|
+
)
|
|
11
15
|
from ai_review.clients.github.pr.schema.pull_request import GitHubGetPRResponseSchema
|
|
12
|
-
from ai_review.clients.github.pr.schema.reviews import
|
|
16
|
+
from ai_review.clients.github.pr.schema.reviews import (
|
|
17
|
+
GitHubGetPRReviewsQuerySchema,
|
|
18
|
+
GitHubGetPRReviewsResponseSchema
|
|
19
|
+
)
|
|
20
|
+
from ai_review.clients.github.pr.types import GitHubPullRequestsHTTPClientProtocol
|
|
13
21
|
from ai_review.libs.http.client import HTTPClient
|
|
14
22
|
from ai_review.libs.http.handlers import HTTPClientError, handle_http_error
|
|
15
23
|
|
|
@@ -18,22 +26,49 @@ class GitHubPullRequestsHTTPClientError(HTTPClientError):
|
|
|
18
26
|
pass
|
|
19
27
|
|
|
20
28
|
|
|
21
|
-
class GitHubPullRequestsHTTPClient(HTTPClient):
|
|
29
|
+
class GitHubPullRequestsHTTPClient(HTTPClient, GitHubPullRequestsHTTPClientProtocol):
|
|
22
30
|
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
23
31
|
async def get_pull_request_api(self, owner: str, repo: str, pull_number: str) -> Response:
|
|
24
32
|
return await self.get(f"/repos/{owner}/{repo}/pulls/{pull_number}")
|
|
25
33
|
|
|
26
34
|
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
27
|
-
async def get_files_api(
|
|
28
|
-
|
|
35
|
+
async def get_files_api(
|
|
36
|
+
self,
|
|
37
|
+
owner: str,
|
|
38
|
+
repo: str,
|
|
39
|
+
pull_number: str,
|
|
40
|
+
query: GitHubGetPRFilesQuerySchema
|
|
41
|
+
) -> Response:
|
|
42
|
+
return await self.get(
|
|
43
|
+
f"/repos/{owner}/{repo}/pulls/{pull_number}/files",
|
|
44
|
+
query=QueryParams(**query.model_dump())
|
|
45
|
+
)
|
|
29
46
|
|
|
30
47
|
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
31
|
-
async def get_issue_comments_api(
|
|
32
|
-
|
|
48
|
+
async def get_issue_comments_api(
|
|
49
|
+
self,
|
|
50
|
+
owner: str,
|
|
51
|
+
repo: str,
|
|
52
|
+
issue_number: str,
|
|
53
|
+
query: GitHubGetPRCommentsQuerySchema,
|
|
54
|
+
) -> Response:
|
|
55
|
+
return await self.get(
|
|
56
|
+
f"/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
|
57
|
+
query=QueryParams(**query.model_dump())
|
|
58
|
+
)
|
|
33
59
|
|
|
34
60
|
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
35
|
-
async def get_review_comments_api(
|
|
36
|
-
|
|
61
|
+
async def get_review_comments_api(
|
|
62
|
+
self,
|
|
63
|
+
owner: str,
|
|
64
|
+
repo: str,
|
|
65
|
+
pull_number: str,
|
|
66
|
+
query: GitHubGetPRCommentsQuerySchema,
|
|
67
|
+
) -> Response:
|
|
68
|
+
return await self.get(
|
|
69
|
+
f"/repos/{owner}/{repo}/pulls/{pull_number}/comments",
|
|
70
|
+
query=QueryParams(**query.model_dump())
|
|
71
|
+
)
|
|
37
72
|
|
|
38
73
|
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
39
74
|
async def create_review_comment_api(
|
|
@@ -62,27 +97,40 @@ class GitHubPullRequestsHTTPClient(HTTPClient):
|
|
|
62
97
|
)
|
|
63
98
|
|
|
64
99
|
@handle_http_error(client="GitHubPullRequestsHTTPClient", exception=GitHubPullRequestsHTTPClientError)
|
|
65
|
-
async def get_reviews_api(
|
|
66
|
-
|
|
100
|
+
async def get_reviews_api(
|
|
101
|
+
self,
|
|
102
|
+
owner: str,
|
|
103
|
+
repo: str,
|
|
104
|
+
pull_number: str,
|
|
105
|
+
query: GitHubGetPRReviewsQuerySchema
|
|
106
|
+
) -> Response:
|
|
107
|
+
return await self.get(
|
|
108
|
+
f"/repos/{owner}/{repo}/pulls/{pull_number}/reviews",
|
|
109
|
+
query=QueryParams(**query.model_dump())
|
|
110
|
+
)
|
|
67
111
|
|
|
68
112
|
async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRResponseSchema:
|
|
69
113
|
response = await self.get_pull_request_api(owner, repo, pull_number)
|
|
70
114
|
return GitHubGetPRResponseSchema.model_validate_json(response.text)
|
|
71
115
|
|
|
72
116
|
async def get_files(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRFilesResponseSchema:
|
|
73
|
-
|
|
117
|
+
query = GitHubGetPRFilesQuerySchema(per_page=100)
|
|
118
|
+
response = await self.get_files_api(owner, repo, pull_number, query)
|
|
74
119
|
return GitHubGetPRFilesResponseSchema.model_validate_json(response.text)
|
|
75
120
|
|
|
76
121
|
async def get_issue_comments(self, owner: str, repo: str, issue_number: str) -> GitHubGetPRCommentsResponseSchema:
|
|
77
|
-
|
|
122
|
+
query = GitHubGetPRCommentsQuerySchema(per_page=100)
|
|
123
|
+
response = await self.get_issue_comments_api(owner, repo, issue_number, query)
|
|
78
124
|
return GitHubGetPRCommentsResponseSchema.model_validate_json(response.text)
|
|
79
125
|
|
|
80
126
|
async def get_review_comments(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRCommentsResponseSchema:
|
|
81
|
-
|
|
127
|
+
query = GitHubGetPRCommentsQuerySchema(per_page=100)
|
|
128
|
+
response = await self.get_review_comments_api(owner, repo, pull_number, query)
|
|
82
129
|
return GitHubGetPRCommentsResponseSchema.model_validate_json(response.text)
|
|
83
130
|
|
|
84
131
|
async def get_reviews(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRReviewsResponseSchema:
|
|
85
|
-
|
|
132
|
+
query = GitHubGetPRReviewsQuerySchema(per_page=100)
|
|
133
|
+
response = await self.get_reviews_api(owner, repo, pull_number, query)
|
|
86
134
|
return GitHubGetPRReviewsResponseSchema.model_validate_json(response.text)
|
|
87
135
|
|
|
88
136
|
async def create_review_comment(
|
|
@@ -8,6 +8,10 @@ class GitHubPRCommentSchema(BaseModel):
|
|
|
8
8
|
line: int | None = None
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class GitHubGetPRCommentsQuerySchema(BaseModel):
|
|
12
|
+
per_page: int
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
class GitHubGetPRCommentsResponseSchema(RootModel[list[GitHubPRCommentSchema]]):
|
|
12
16
|
root: list[GitHubPRCommentSchema]
|
|
13
17
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.github.pr.schema.comments import (
|
|
4
|
+
GitHubGetPRCommentsResponseSchema,
|
|
5
|
+
GitHubCreateIssueCommentResponseSchema,
|
|
6
|
+
GitHubCreateReviewCommentResponseSchema,
|
|
7
|
+
GitHubCreateReviewCommentRequestSchema,
|
|
8
|
+
)
|
|
9
|
+
from ai_review.clients.github.pr.schema.files import GitHubGetPRFilesResponseSchema
|
|
10
|
+
from ai_review.clients.github.pr.schema.pull_request import GitHubGetPRResponseSchema
|
|
11
|
+
from ai_review.clients.github.pr.schema.reviews import GitHubGetPRReviewsResponseSchema
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GitHubPullRequestsHTTPClientProtocol(Protocol):
|
|
15
|
+
async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRResponseSchema: ...
|
|
16
|
+
|
|
17
|
+
async def get_files(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRFilesResponseSchema: ...
|
|
18
|
+
|
|
19
|
+
async def get_issue_comments(
|
|
20
|
+
self,
|
|
21
|
+
owner: str,
|
|
22
|
+
repo: str,
|
|
23
|
+
issue_number: str
|
|
24
|
+
) -> GitHubGetPRCommentsResponseSchema: ...
|
|
25
|
+
|
|
26
|
+
async def get_review_comments(
|
|
27
|
+
self,
|
|
28
|
+
owner: str,
|
|
29
|
+
repo: str,
|
|
30
|
+
pull_number: str
|
|
31
|
+
) -> GitHubGetPRCommentsResponseSchema: ...
|
|
32
|
+
|
|
33
|
+
async def get_reviews(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRReviewsResponseSchema: ...
|
|
34
|
+
|
|
35
|
+
async def create_review_comment(
|
|
36
|
+
self,
|
|
37
|
+
owner: str,
|
|
38
|
+
repo: str,
|
|
39
|
+
pull_number: str,
|
|
40
|
+
request: GitHubCreateReviewCommentRequestSchema,
|
|
41
|
+
) -> GitHubCreateReviewCommentResponseSchema: ...
|
|
42
|
+
|
|
43
|
+
async def create_issue_comment(
|
|
44
|
+
self,
|
|
45
|
+
owner: str,
|
|
46
|
+
repo: str,
|
|
47
|
+
issue_number: str,
|
|
48
|
+
body: str,
|
|
49
|
+
) -> GitHubCreateIssueCommentResponseSchema: ...
|
|
@@ -15,7 +15,7 @@ class GitLabHTTPClient:
|
|
|
15
15
|
def get_gitlab_http_client() -> GitLabHTTPClient:
|
|
16
16
|
logger = get_logger("GITLAB_HTTP_CLIENT")
|
|
17
17
|
logger_event_hook = LoggerEventHook(logger=logger)
|
|
18
|
-
retry_transport = RetryTransport(transport=AsyncHTTPTransport())
|
|
18
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
19
19
|
|
|
20
20
|
client = AsyncClient(
|
|
21
21
|
timeout=settings.llm.http_client.timeout,
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
from httpx import Response
|
|
1
|
+
from httpx import Response, QueryParams
|
|
2
2
|
|
|
3
3
|
from ai_review.clients.gitlab.mr.schema.changes import GitLabGetMRChangesResponseSchema
|
|
4
4
|
from ai_review.clients.gitlab.mr.schema.discussions import (
|
|
5
|
+
GitLabGetMRDiscussionsQuerySchema,
|
|
5
6
|
GitLabGetMRDiscussionsResponseSchema,
|
|
6
7
|
GitLabCreateMRDiscussionRequestSchema,
|
|
7
8
|
GitLabCreateMRDiscussionResponseSchema
|
|
8
9
|
)
|
|
9
10
|
from ai_review.clients.gitlab.mr.schema.notes import (
|
|
11
|
+
GitLabGetMRNotesQuerySchema,
|
|
10
12
|
GitLabGetMRNotesResponseSchema,
|
|
11
13
|
GitLabCreateMRNoteRequestSchema,
|
|
12
14
|
GitLabCreateMRNoteResponseSchema,
|
|
13
15
|
)
|
|
16
|
+
from ai_review.clients.gitlab.mr.types import GitLabMergeRequestsHTTPClientProtocol
|
|
14
17
|
from ai_review.libs.http.client import HTTPClient
|
|
15
18
|
from ai_review.libs.http.handlers import handle_http_error, HTTPClientError
|
|
16
19
|
|
|
@@ -19,7 +22,7 @@ class GitLabMergeRequestsHTTPClientError(HTTPClientError):
|
|
|
19
22
|
pass
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
25
|
+
class GitLabMergeRequestsHTTPClient(HTTPClient, GitLabMergeRequestsHTTPClientProtocol):
|
|
23
26
|
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
24
27
|
async def get_changes_api(self, project_id: str, merge_request_id: str) -> Response:
|
|
25
28
|
return await self.get(
|
|
@@ -27,15 +30,27 @@ class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
30
|
-
async def get_notes_api(
|
|
33
|
+
async def get_notes_api(
|
|
34
|
+
self,
|
|
35
|
+
project_id: str,
|
|
36
|
+
merge_request_id: str,
|
|
37
|
+
query: GitLabGetMRNotesQuerySchema
|
|
38
|
+
) -> Response:
|
|
31
39
|
return await self.get(
|
|
32
|
-
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/notes"
|
|
40
|
+
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/notes",
|
|
41
|
+
query=QueryParams(**query.model_dump())
|
|
33
42
|
)
|
|
34
43
|
|
|
35
44
|
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
36
|
-
async def get_discussions_api(
|
|
45
|
+
async def get_discussions_api(
|
|
46
|
+
self,
|
|
47
|
+
project_id: str,
|
|
48
|
+
merge_request_id: str,
|
|
49
|
+
query: GitLabGetMRDiscussionsQuerySchema
|
|
50
|
+
) -> Response:
|
|
37
51
|
return await self.get(
|
|
38
|
-
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/discussions"
|
|
52
|
+
f"/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/discussions",
|
|
53
|
+
query=QueryParams(**query.model_dump())
|
|
39
54
|
)
|
|
40
55
|
|
|
41
56
|
@handle_http_error(client="GitLabMergeRequestsHTTPClient", exception=GitLabMergeRequestsHTTPClientError)
|
|
@@ -71,7 +86,8 @@ class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
|
71
86
|
project_id: str,
|
|
72
87
|
merge_request_id: str
|
|
73
88
|
) -> GitLabGetMRNotesResponseSchema:
|
|
74
|
-
|
|
89
|
+
query = GitLabGetMRNotesQuerySchema(per_page=100)
|
|
90
|
+
response = await self.get_notes_api(project_id, merge_request_id, query)
|
|
75
91
|
return GitLabGetMRNotesResponseSchema.model_validate_json(response.text)
|
|
76
92
|
|
|
77
93
|
async def get_discussions(
|
|
@@ -79,7 +95,8 @@ class GitLabMergeRequestsHTTPClient(HTTPClient):
|
|
|
79
95
|
project_id: str,
|
|
80
96
|
merge_request_id: str
|
|
81
97
|
) -> GitLabGetMRDiscussionsResponseSchema:
|
|
82
|
-
|
|
98
|
+
query = GitLabGetMRDiscussionsQuerySchema(per_page=100)
|
|
99
|
+
response = await self.get_discussions_api(project_id, merge_request_id, query)
|
|
83
100
|
return GitLabGetMRDiscussionsResponseSchema.model_validate_json(response.text)
|
|
84
101
|
|
|
85
102
|
async def create_note(
|
|
@@ -17,6 +17,10 @@ class GitLabDiscussionPositionSchema(BaseModel):
|
|
|
17
17
|
new_line: int
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class GitLabGetMRDiscussionsQuerySchema(BaseModel):
|
|
21
|
+
per_page: int
|
|
22
|
+
|
|
23
|
+
|
|
20
24
|
class GitLabGetMRDiscussionsResponseSchema(RootModel[list[GitLabDiscussionSchema]]):
|
|
21
25
|
root: list[GitLabDiscussionSchema]
|
|
22
26
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.gitlab.mr.schema.changes import GitLabGetMRChangesResponseSchema
|
|
4
|
+
from ai_review.clients.gitlab.mr.schema.discussions import (
|
|
5
|
+
GitLabGetMRDiscussionsResponseSchema,
|
|
6
|
+
GitLabCreateMRDiscussionResponseSchema,
|
|
7
|
+
GitLabCreateMRDiscussionRequestSchema,
|
|
8
|
+
)
|
|
9
|
+
from ai_review.clients.gitlab.mr.schema.notes import GitLabGetMRNotesResponseSchema, GitLabCreateMRNoteResponseSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitLabMergeRequestsHTTPClientProtocol(Protocol):
|
|
13
|
+
async def get_changes(self, project_id: str, merge_request_id: str) -> GitLabGetMRChangesResponseSchema: ...
|
|
14
|
+
|
|
15
|
+
async def get_notes(self, project_id: str, merge_request_id: str) -> GitLabGetMRNotesResponseSchema: ...
|
|
16
|
+
|
|
17
|
+
async def get_discussions(
|
|
18
|
+
self,
|
|
19
|
+
project_id: str,
|
|
20
|
+
merge_request_id: str
|
|
21
|
+
) -> GitLabGetMRDiscussionsResponseSchema: ...
|
|
22
|
+
|
|
23
|
+
async def create_note(
|
|
24
|
+
self,
|
|
25
|
+
body: str,
|
|
26
|
+
project_id: str,
|
|
27
|
+
merge_request_id: str,
|
|
28
|
+
) -> GitLabCreateMRNoteResponseSchema: ...
|
|
29
|
+
|
|
30
|
+
async def create_discussion(
|
|
31
|
+
self,
|
|
32
|
+
project_id: str,
|
|
33
|
+
merge_request_id: str,
|
|
34
|
+
request: GitLabCreateMRDiscussionRequestSchema,
|
|
35
|
+
) -> GitLabCreateMRDiscussionResponseSchema: ...
|
|
@@ -26,7 +26,7 @@ class OpenAIHTTPClient(HTTPClient):
|
|
|
26
26
|
def get_openai_http_client() -> OpenAIHTTPClient:
|
|
27
27
|
logger = get_logger("OPENAI_HTTP_CLIENT")
|
|
28
28
|
logger_event_hook = LoggerEventHook(logger=logger)
|
|
29
|
-
retry_transport = RetryTransport(transport=AsyncHTTPTransport())
|
|
29
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
30
30
|
|
|
31
31
|
client = AsyncClient(
|
|
32
32
|
timeout=settings.llm.http_client.timeout,
|
ai_review/config.py
CHANGED
|
@@ -12,6 +12,7 @@ from ai_review.libs.config.base import (
|
|
|
12
12
|
get_yaml_config_file_or_default,
|
|
13
13
|
get_json_config_file_or_default
|
|
14
14
|
)
|
|
15
|
+
from ai_review.libs.config.core import CoreConfig
|
|
15
16
|
from ai_review.libs.config.llm import LLMConfig
|
|
16
17
|
from ai_review.libs.config.logger import LoggerConfig
|
|
17
18
|
from ai_review.libs.config.prompt import PromptConfig
|
|
@@ -36,6 +37,7 @@ class Settings(BaseSettings):
|
|
|
36
37
|
|
|
37
38
|
llm: LLMConfig
|
|
38
39
|
vcs: VCSConfig
|
|
40
|
+
core: CoreConfig = CoreConfig()
|
|
39
41
|
prompt: PromptConfig = PromptConfig()
|
|
40
42
|
review: ReviewConfig = ReviewConfig()
|
|
41
43
|
logger: LoggerConfig = LoggerConfig()
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import Awaitable, Iterable, TypeVar
|
|
3
3
|
|
|
4
|
+
from ai_review.config import settings
|
|
5
|
+
|
|
4
6
|
T = TypeVar("T")
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
async def bounded_gather(coroutines: Iterable[Awaitable[T]]
|
|
8
|
-
sem = asyncio.Semaphore(concurrency)
|
|
9
|
+
async def bounded_gather(coroutines: Iterable[Awaitable[T]]) -> tuple[T, ...]:
|
|
10
|
+
sem = asyncio.Semaphore(settings.core.concurrency)
|
|
9
11
|
|
|
10
12
|
async def wrap(coro: Awaitable[T]) -> T:
|
|
11
13
|
async with sem:
|
|
12
14
|
return await coro
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
results = await asyncio.gather(*(wrap(coroutine) for coroutine in coroutines), return_exceptions=True)
|
|
17
|
+
return tuple(results)
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from httpx import Request, Response
|
|
4
4
|
|
|
5
5
|
from ai_review.libs.http.event_hooks.base import BaseEventHook
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from loguru import Logger
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class LoggerEventHook(BaseEventHook):
|
|
9
|
-
def __init__(self, logger: Logger):
|
|
12
|
+
def __init__(self, logger: "Logger"):
|
|
10
13
|
self.logger = logger
|
|
11
14
|
|
|
12
15
|
async def request(self, request: Request):
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from http import HTTPStatus
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
|
|
4
5
|
from httpx import Request, Response, AsyncBaseTransport
|
|
5
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from loguru import Logger
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
class RetryTransport(AsyncBaseTransport):
|
|
8
12
|
def __init__(
|
|
9
13
|
self,
|
|
14
|
+
logger: "Logger",
|
|
10
15
|
transport: AsyncBaseTransport,
|
|
11
16
|
max_retries: int = 5,
|
|
12
17
|
retry_delay: float = 0.5,
|
|
@@ -17,18 +22,30 @@ class RetryTransport(AsyncBaseTransport):
|
|
|
17
22
|
HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
18
23
|
)
|
|
19
24
|
):
|
|
25
|
+
self.logger = logger
|
|
20
26
|
self.transport = transport
|
|
21
27
|
self.max_retries = max_retries
|
|
22
28
|
self.retry_delay = retry_delay
|
|
23
29
|
self.retry_status_codes = retry_status_codes
|
|
24
30
|
|
|
25
31
|
async def handle_async_request(self, request: Request) -> Response:
|
|
26
|
-
|
|
27
|
-
for
|
|
28
|
-
|
|
29
|
-
if
|
|
30
|
-
return
|
|
32
|
+
last_response: Response | None = None
|
|
33
|
+
for attempt in range(self.max_retries):
|
|
34
|
+
last_response = await self.transport.handle_async_request(request)
|
|
35
|
+
if last_response.status_code not in self.retry_status_codes:
|
|
36
|
+
return last_response
|
|
37
|
+
|
|
38
|
+
self.logger.warning(
|
|
39
|
+
f"Attempt {attempt}/{self.max_retries} failed "
|
|
40
|
+
f"with status={last_response.status_code} for {request.method} {request.url}. "
|
|
41
|
+
f"Retrying in {self.retry_delay:.1f}s..."
|
|
42
|
+
)
|
|
31
43
|
|
|
32
44
|
await asyncio.sleep(self.retry_delay)
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
self.logger.error(
|
|
47
|
+
f"All {self.max_retries} attempts failed for "
|
|
48
|
+
f"{request.method} {request.url} (last status={last_response.status_code})"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return last_response
|
|
@@ -6,11 +6,12 @@ from ai_review.config import settings
|
|
|
6
6
|
from ai_review.libs.logger import get_logger
|
|
7
7
|
from ai_review.services.artifacts.schema import LLMArtifactSchema
|
|
8
8
|
from ai_review.services.artifacts.tools import make_artifact_id
|
|
9
|
+
from ai_review.services.artifacts.types import ArtifactsServiceProtocol
|
|
9
10
|
|
|
10
11
|
logger = get_logger("ARTIFACTS_SERVICE")
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class ArtifactsService:
|
|
14
|
+
class ArtifactsService(ArtifactsServiceProtocol):
|
|
14
15
|
@classmethod
|
|
15
16
|
async def save_llm_interaction(cls, prompt: str, prompt_system: str, response: str | None = None) -> str | None:
|
|
16
17
|
if not settings.artifacts.llm_enabled:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ArtifactsServiceProtocol(Protocol):
|
|
6
|
+
async def save_llm_interaction(
|
|
7
|
+
self,
|
|
8
|
+
prompt: str,
|
|
9
|
+
prompt_system: str,
|
|
10
|
+
response: str | None = None
|
|
11
|
+
) -> str | None:
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
async def save_artifact(
|
|
15
|
+
self,
|
|
16
|
+
file: Path,
|
|
17
|
+
content: str,
|
|
18
|
+
kind: str = "artifact"
|
|
19
|
+
) -> Path | None:
|
|
20
|
+
...
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from ai_review.config import settings
|
|
2
2
|
from ai_review.libs.logger import get_logger
|
|
3
3
|
from ai_review.services.cost.schema import CostReportSchema
|
|
4
|
+
from ai_review.services.cost.types import CostServiceProtocol
|
|
4
5
|
from ai_review.services.llm.types import ChatResultSchema
|
|
5
6
|
|
|
6
7
|
logger = get_logger("COST_SERVICE")
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class CostService:
|
|
10
|
+
class CostService(CostServiceProtocol):
|
|
10
11
|
def __init__(self):
|
|
11
12
|
self.pricing = settings.llm.load_pricing()
|
|
12
13
|
self.reports: list[CostReportSchema] = []
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.services.cost.schema import CostReportSchema
|
|
4
|
+
from ai_review.services.llm.types import ChatResultSchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CostServiceProtocol(Protocol):
|
|
8
|
+
def calculate(self, result: ChatResultSchema) -> CostReportSchema | None:
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
def aggregate(self) -> CostReportSchema | None:
|
|
12
|
+
...
|
|
@@ -16,12 +16,13 @@ from ai_review.services.diff.renderers import (
|
|
|
16
16
|
)
|
|
17
17
|
from ai_review.services.diff.schema import DiffFileSchema
|
|
18
18
|
from ai_review.services.diff.tools import find_diff_file
|
|
19
|
+
from ai_review.services.diff.types import DiffServiceProtocol
|
|
19
20
|
from ai_review.services.git.types import GitServiceProtocol
|
|
20
21
|
|
|
21
22
|
logger = get_logger("DIFF_SERVICE")
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
class DiffService:
|
|
25
|
+
class DiffService(DiffServiceProtocol):
|
|
25
26
|
@classmethod
|
|
26
27
|
def parse(cls, raw_diff: str) -> Diff:
|
|
27
28
|
if not raw_diff.strip():
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.diff.models import Diff
|
|
4
|
+
from ai_review.services.diff.schema import DiffFileSchema
|
|
5
|
+
from ai_review.services.git.types import GitServiceProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DiffServiceProtocol(Protocol):
|
|
9
|
+
def parse(self, raw_diff: str) -> Diff:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
def render_file(
|
|
13
|
+
self,
|
|
14
|
+
file: str,
|
|
15
|
+
raw_diff: str,
|
|
16
|
+
base_sha: str | None = None,
|
|
17
|
+
head_sha: str | None = None,
|
|
18
|
+
) -> DiffFileSchema:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def render_files(
|
|
22
|
+
self,
|
|
23
|
+
git: GitServiceProtocol,
|
|
24
|
+
files: list[str],
|
|
25
|
+
base_sha: str,
|
|
26
|
+
head_sha: str,
|
|
27
|
+
) -> list[DiffFileSchema]:
|
|
28
|
+
...
|