xai-review 0.21.0__py3-none-any.whl → 0.23.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/diff/renderers.py +3 -3
- ai_review/services/prompt/schema.py +13 -24
- 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/review/__init__.py +0 -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 +1 -1
- ai_review/tests/suites/services/diff/test_tools.py +1 -1
- 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/prompt/test_schema.py +65 -0
- ai_review/tests/suites/services/review/test_service.py +9 -9
- 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.21.0.dist-info → xai_review-0.23.0.dist-info}/METADATA +2 -2
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/RECORD +63 -43
- /ai_review/tests/fixtures/{review → clients}/__init__.py +0 -0
- /ai_review/tests/fixtures/{artifacts.py → services/artifacts.py} +0 -0
- /ai_review/tests/fixtures/{cost.py → services/cost.py} +0 -0
- /ai_review/tests/fixtures/{diff.py → services/diff.py} +0 -0
- /ai_review/tests/fixtures/{git.py → services/git.py} +0 -0
- /ai_review/tests/fixtures/{llm.py → services/llm.py} +0 -0
- /ai_review/tests/fixtures/{prompt.py → services/prompt.py} +0 -0
- /ai_review/tests/fixtures/{review → services/review}/inline.py +0 -0
- /ai_review/tests/fixtures/{review → services/review}/summary.py +0 -0
- /ai_review/tests/fixtures/{vcs.py → services/vcs.py} +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/WHEEL +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.21.0.dist-info → xai_review-0.23.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
|
|
@@ -116,14 +116,14 @@ def render_unified(
|
|
|
116
116
|
added_new_positions = file.added_line_numbers()
|
|
117
117
|
removed_old_positions = file.removed_line_numbers()
|
|
118
118
|
|
|
119
|
-
def in_context(
|
|
119
|
+
def in_context(inner_old_no: int | None, inner_new_no: int | None) -> bool:
|
|
120
120
|
"""Check if an unchanged line falls within context radius."""
|
|
121
121
|
if context <= 0:
|
|
122
122
|
return False
|
|
123
|
-
if include_added and
|
|
123
|
+
if include_added and inner_new_no is not None:
|
|
124
124
|
if any(abs(new_no - a) <= context for a in added_new_positions):
|
|
125
125
|
return True
|
|
126
|
-
if include_removed and
|
|
126
|
+
if include_removed and inner_old_no is not None:
|
|
127
127
|
if any(abs(old_no - r) <= context for r in removed_old_positions):
|
|
128
128
|
return True
|
|
129
129
|
return False
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
1
|
+
from pydantic import BaseModel, Field, field_serializer
|
|
2
2
|
|
|
3
3
|
from ai_review.config import settings
|
|
4
4
|
from ai_review.libs.template.render import render_template
|
|
@@ -24,29 +24,18 @@ class PromptContextSchema(BaseModel):
|
|
|
24
24
|
labels: list[str] = Field(default_factory=list)
|
|
25
25
|
changed_files: list[str] = Field(default_factory=list)
|
|
26
26
|
|
|
27
|
-
@
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"review_reviewers_usernames": ", ".join(self.review_reviewers_usernames),
|
|
39
|
-
|
|
40
|
-
"review_assignees": ", ".join(self.review_assignees),
|
|
41
|
-
"review_assignees_usernames": ", ".join(self.review_assignees_usernames),
|
|
42
|
-
|
|
43
|
-
"source_branch": self.source_branch,
|
|
44
|
-
"target_branch": self.target_branch,
|
|
45
|
-
|
|
46
|
-
"labels": ", ".join(self.labels),
|
|
47
|
-
"changed_files": ", ".join(self.changed_files),
|
|
48
|
-
}
|
|
27
|
+
@field_serializer(
|
|
28
|
+
"review_reviewers",
|
|
29
|
+
"review_reviewers_usernames",
|
|
30
|
+
"review_assignees",
|
|
31
|
+
"review_assignees_usernames",
|
|
32
|
+
"labels",
|
|
33
|
+
"changed_files",
|
|
34
|
+
when_used="always"
|
|
35
|
+
)
|
|
36
|
+
def list_of_strings_serializer(self, value: list[str]) -> str:
|
|
37
|
+
return ", ".join(value)
|
|
49
38
|
|
|
50
39
|
def apply_format(self, prompt: str) -> str:
|
|
51
|
-
values = {**self.
|
|
40
|
+
values = {**self.model_dump(), **settings.prompt.context}
|
|
52
41
|
return render_template(prompt, values, settings.prompt.context_placeholder)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import HttpUrl, SecretStr
|
|
3
|
+
|
|
4
|
+
from ai_review.config import settings
|
|
5
|
+
from ai_review.libs.config.claude import ClaudeMetaConfig, ClaudeHTTPClientConfig
|
|
6
|
+
from ai_review.libs.config.llm import ClaudeLLMConfig
|
|
7
|
+
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def claude_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
12
|
+
fake_config = ClaudeLLMConfig(
|
|
13
|
+
meta=ClaudeMetaConfig(),
|
|
14
|
+
provider=LLMProvider.CLAUDE,
|
|
15
|
+
http_client=ClaudeHTTPClientConfig(
|
|
16
|
+
timeout=10,
|
|
17
|
+
api_url=HttpUrl("https://api.anthropic.com"),
|
|
18
|
+
api_token=SecretStr("fake-token"),
|
|
19
|
+
api_version="2023-06-01",
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
monkeypatch.setattr(settings, "llm", fake_config)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import HttpUrl, SecretStr
|
|
3
|
+
|
|
4
|
+
from ai_review.config import settings
|
|
5
|
+
from ai_review.libs.config.gemini import GeminiMetaConfig, GeminiHTTPClientConfig
|
|
6
|
+
from ai_review.libs.config.llm import GeminiLLMConfig
|
|
7
|
+
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def gemini_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
12
|
+
fake_config = GeminiLLMConfig(
|
|
13
|
+
meta=GeminiMetaConfig(),
|
|
14
|
+
provider=LLMProvider.GEMINI,
|
|
15
|
+
http_client=GeminiHTTPClientConfig(
|
|
16
|
+
timeout=10,
|
|
17
|
+
api_url=HttpUrl("https://generativelanguage.googleapis.com"),
|
|
18
|
+
api_token=SecretStr("fake-token"),
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
monkeypatch.setattr(settings, "llm", fake_config)
|