xai-review 0.18.0__py3-none-any.whl → 0.19.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/github/pr/client.py +13 -7
- ai_review/clients/github/pr/schema/pull_request.py +6 -6
- ai_review/clients/gitlab/mr/client.py +29 -20
- ai_review/clients/gitlab/mr/schema/changes.py +5 -5
- ai_review/clients/gitlab/mr/schema/discussions.py +1 -4
- ai_review/clients/gitlab/mr/schema/notes.py +19 -0
- ai_review/services/llm/factory.py +1 -1
- ai_review/services/prompt/adapter.py +15 -15
- ai_review/services/prompt/schema.py +18 -18
- ai_review/services/review/service.py +45 -42
- ai_review/services/vcs/factory.py +1 -1
- ai_review/services/vcs/github/client.py +52 -34
- ai_review/services/vcs/gitlab/client.py +62 -44
- ai_review/services/vcs/types.py +38 -29
- ai_review/tests/suites/services/cost/__init__.py +0 -0
- ai_review/tests/suites/services/cost/test_schema.py +124 -0
- ai_review/tests/suites/services/cost/test_service.py +99 -0
- ai_review/tests/suites/services/prompt/test_adapter.py +59 -0
- ai_review/tests/suites/services/prompt/test_schema.py +18 -18
- ai_review/tests/suites/services/prompt/test_service.py +13 -11
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/METADATA +21 -6
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/RECORD +26 -22
- ai_review/clients/gitlab/mr/schema/comments.py +0 -19
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/WHEEL +0 -0
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.18.0.dist-info → xai_review-0.19.0.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from ai_review.clients.github.client import get_github_http_client
|
|
2
|
+
from ai_review.clients.github.pr.schema.comments import GitHubCreateReviewCommentRequestSchema
|
|
2
3
|
from ai_review.config import settings
|
|
3
4
|
from ai_review.libs.logger import get_logger
|
|
4
5
|
from ai_review.services.vcs.types import (
|
|
5
6
|
VCSClient,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
MRDiscussionSchema,
|
|
7
|
+
UserSchema,
|
|
8
|
+
BranchRefSchema,
|
|
9
|
+
ReviewInfoSchema,
|
|
10
|
+
ReviewCommentSchema,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
logger = get_logger("GITHUB_VCS_CLIENT")
|
|
@@ -20,7 +20,7 @@ class GitHubVCSClient(VCSClient):
|
|
|
20
20
|
self.repo = settings.vcs.pipeline.repo
|
|
21
21
|
self.pull_number = settings.vcs.pipeline.pull_number
|
|
22
22
|
|
|
23
|
-
async def
|
|
23
|
+
async def get_review_info(self) -> ReviewInfoSchema:
|
|
24
24
|
try:
|
|
25
25
|
pr = await self.http_client.pr.get_pull_request(
|
|
26
26
|
owner=self.owner, repo=self.repo, pull_number=self.pull_number
|
|
@@ -33,35 +33,44 @@ class GitHubVCSClient(VCSClient):
|
|
|
33
33
|
f"Fetched PR info for {self.owner}/{self.repo}#{self.pull_number}"
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
return
|
|
36
|
+
return ReviewInfoSchema(
|
|
37
|
+
id=pr.number,
|
|
37
38
|
title=pr.title,
|
|
38
|
-
|
|
39
|
+
description=pr.body or "",
|
|
40
|
+
author=UserSchema(
|
|
41
|
+
id=pr.user.id,
|
|
39
42
|
name=pr.user.login,
|
|
40
43
|
username=pr.user.login,
|
|
41
44
|
),
|
|
42
|
-
labels=[label.name for label in pr.labels],
|
|
45
|
+
labels=[label.name for label in pr.labels if label.name],
|
|
43
46
|
base_sha=pr.base.sha,
|
|
44
47
|
head_sha=pr.head.sha,
|
|
45
48
|
assignees=[
|
|
46
|
-
|
|
49
|
+
UserSchema(id=user.id, name=user.login, username=user.login)
|
|
47
50
|
for user in pr.assignees
|
|
48
51
|
],
|
|
49
52
|
reviewers=[
|
|
50
|
-
|
|
53
|
+
UserSchema(id=user.id, name=user.login, username=user.login)
|
|
51
54
|
for user in pr.requested_reviewers
|
|
52
55
|
],
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
source_branch=BranchRefSchema(
|
|
57
|
+
ref=pr.head.ref,
|
|
58
|
+
sha=pr.head.sha,
|
|
59
|
+
),
|
|
60
|
+
target_branch=BranchRefSchema(
|
|
61
|
+
ref=pr.base.ref,
|
|
62
|
+
sha=pr.base.sha,
|
|
63
|
+
),
|
|
56
64
|
changed_files=[file.filename for file in files.root],
|
|
57
65
|
)
|
|
58
66
|
except Exception as error:
|
|
59
67
|
logger.exception(
|
|
60
68
|
f"Failed to fetch PR info {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
61
69
|
)
|
|
62
|
-
return
|
|
70
|
+
return ReviewInfoSchema()
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
# === GENERAL COMMENTS ===
|
|
73
|
+
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
65
74
|
try:
|
|
66
75
|
response = await self.http_client.pr.get_issue_comments(
|
|
67
76
|
owner=self.owner,
|
|
@@ -69,17 +78,20 @@ class GitHubVCSClient(VCSClient):
|
|
|
69
78
|
issue_number=self.pull_number,
|
|
70
79
|
)
|
|
71
80
|
logger.info(
|
|
72
|
-
f"Fetched
|
|
81
|
+
f"Fetched general comments for {self.owner}/{self.repo}#{self.pull_number}"
|
|
73
82
|
)
|
|
74
83
|
|
|
75
|
-
return [
|
|
84
|
+
return [
|
|
85
|
+
ReviewCommentSchema(id=comment.id, body=comment.body or "")
|
|
86
|
+
for comment in response.root
|
|
87
|
+
]
|
|
76
88
|
except Exception as error:
|
|
77
89
|
logger.exception(
|
|
78
|
-
f"Failed to fetch
|
|
90
|
+
f"Failed to fetch general comments for {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
79
91
|
)
|
|
80
92
|
return []
|
|
81
93
|
|
|
82
|
-
async def
|
|
94
|
+
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
83
95
|
try:
|
|
84
96
|
response = await self.http_client.pr.get_review_comments(
|
|
85
97
|
owner=self.owner,
|
|
@@ -87,23 +99,25 @@ class GitHubVCSClient(VCSClient):
|
|
|
87
99
|
pull_number=self.pull_number,
|
|
88
100
|
)
|
|
89
101
|
logger.info(
|
|
90
|
-
f"Fetched
|
|
102
|
+
f"Fetched inline comments for {self.owner}/{self.repo}#{self.pull_number}"
|
|
91
103
|
)
|
|
92
104
|
|
|
93
105
|
return [
|
|
94
|
-
|
|
95
|
-
id=
|
|
96
|
-
|
|
106
|
+
ReviewCommentSchema(
|
|
107
|
+
id=comment.id,
|
|
108
|
+
body=comment.body or "",
|
|
109
|
+
file=comment.path,
|
|
110
|
+
line=comment.line,
|
|
97
111
|
)
|
|
98
112
|
for comment in response.root
|
|
99
113
|
]
|
|
100
114
|
except Exception as error:
|
|
101
115
|
logger.exception(
|
|
102
|
-
f"Failed to fetch
|
|
116
|
+
f"Failed to fetch inline comments for {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
103
117
|
)
|
|
104
118
|
return []
|
|
105
119
|
|
|
106
|
-
async def
|
|
120
|
+
async def create_general_comment(self, message: str) -> None:
|
|
107
121
|
try:
|
|
108
122
|
logger.info(
|
|
109
123
|
f"Posting general comment to PR {self.owner}/{self.repo}#{self.pull_number}: {message}"
|
|
@@ -122,30 +136,34 @@ class GitHubVCSClient(VCSClient):
|
|
|
122
136
|
f"Failed to create general comment in PR {self.owner}/{self.repo}#{self.pull_number}: {error}"
|
|
123
137
|
)
|
|
124
138
|
|
|
125
|
-
async def
|
|
139
|
+
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
126
140
|
try:
|
|
127
141
|
logger.info(
|
|
128
|
-
f"Posting inline comment in {self.owner}/{self.repo}#{self.pull_number}
|
|
142
|
+
f"Posting inline comment in {self.owner}/{self.repo}#{self.pull_number} "
|
|
143
|
+
f"at {file}:{line}: {message}"
|
|
129
144
|
)
|
|
130
145
|
|
|
131
146
|
pr = await self.http_client.pr.get_pull_request(
|
|
132
147
|
owner=self.owner, repo=self.repo, pull_number=self.pull_number
|
|
133
148
|
)
|
|
134
|
-
commit_id = pr.head.sha
|
|
135
149
|
|
|
150
|
+
request = GitHubCreateReviewCommentRequestSchema(
|
|
151
|
+
body=message,
|
|
152
|
+
path=file,
|
|
153
|
+
line=line,
|
|
154
|
+
commit_id=pr.head.sha
|
|
155
|
+
)
|
|
136
156
|
await self.http_client.pr.create_review_comment(
|
|
137
157
|
owner=self.owner,
|
|
138
158
|
repo=self.repo,
|
|
139
159
|
pull_number=self.pull_number,
|
|
140
|
-
|
|
141
|
-
path=file,
|
|
142
|
-
line=line,
|
|
143
|
-
commit_id=commit_id,
|
|
160
|
+
request=request,
|
|
144
161
|
)
|
|
145
162
|
logger.info(
|
|
146
163
|
f"Created inline comment in {self.owner}/{self.repo}#{self.pull_number} at {file}:{line}"
|
|
147
164
|
)
|
|
148
165
|
except Exception as error:
|
|
149
166
|
logger.exception(
|
|
150
|
-
f"Failed to create inline comment in {self.owner}/{self.repo}#{self.pull_number}
|
|
167
|
+
f"Failed to create inline comment in {self.owner}/{self.repo}#{self.pull_number} "
|
|
168
|
+
f"at {file}:{line}: {error}"
|
|
151
169
|
)
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
from ai_review.clients.gitlab.client import get_gitlab_http_client
|
|
2
2
|
from ai_review.clients.gitlab.mr.schema.discussions import (
|
|
3
3
|
GitLabDiscussionPositionSchema,
|
|
4
|
-
GitLabCreateMRDiscussionRequestSchema
|
|
4
|
+
GitLabCreateMRDiscussionRequestSchema,
|
|
5
5
|
)
|
|
6
6
|
from ai_review.config import settings
|
|
7
7
|
from ai_review.libs.logger import get_logger
|
|
8
8
|
from ai_review.services.vcs.types import (
|
|
9
9
|
VCSClient,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
MRDiscussionSchema,
|
|
10
|
+
UserSchema,
|
|
11
|
+
BranchRefSchema,
|
|
12
|
+
ReviewInfoSchema,
|
|
13
|
+
ReviewCommentSchema,
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
logger = get_logger("GITLAB_VCS_CLIENT")
|
|
@@ -23,104 +22,121 @@ class GitLabVCSClient(VCSClient):
|
|
|
23
22
|
self.project_id = settings.vcs.pipeline.project_id
|
|
24
23
|
self.merge_request_id = settings.vcs.pipeline.merge_request_id
|
|
25
24
|
|
|
26
|
-
async def
|
|
25
|
+
async def get_review_info(self) -> ReviewInfoSchema:
|
|
27
26
|
try:
|
|
28
27
|
response = await self.http_client.mr.get_changes(
|
|
29
28
|
project_id=self.project_id,
|
|
30
29
|
merge_request_id=self.merge_request_id,
|
|
31
30
|
)
|
|
32
|
-
logger.info(
|
|
31
|
+
logger.info(
|
|
32
|
+
f"Fetched MR info for project_id={self.project_id} merge_request_id={self.merge_request_id}"
|
|
33
|
+
)
|
|
33
34
|
|
|
34
|
-
return
|
|
35
|
+
return ReviewInfoSchema(
|
|
36
|
+
id=response.iid,
|
|
35
37
|
title=response.title,
|
|
36
|
-
|
|
38
|
+
description=response.description,
|
|
39
|
+
author=UserSchema(
|
|
37
40
|
name=response.author.name,
|
|
38
|
-
username=response.author.username
|
|
41
|
+
username=response.author.username,
|
|
42
|
+
id=response.author.id,
|
|
39
43
|
),
|
|
40
44
|
labels=response.labels,
|
|
41
45
|
base_sha=response.diff_refs.base_sha,
|
|
42
46
|
head_sha=response.diff_refs.head_sha,
|
|
43
47
|
start_sha=response.diff_refs.start_sha,
|
|
44
48
|
reviewers=[
|
|
45
|
-
|
|
46
|
-
for
|
|
49
|
+
UserSchema(id=user.id, name=user.name, username=user.username)
|
|
50
|
+
for user in response.reviewers
|
|
47
51
|
],
|
|
48
52
|
assignees=[
|
|
49
|
-
|
|
50
|
-
for
|
|
53
|
+
UserSchema(id=user.id, name=user.name, username=user.username)
|
|
54
|
+
for user in response.assignees
|
|
55
|
+
],
|
|
56
|
+
source_branch=BranchRefSchema(
|
|
57
|
+
ref=response.source_branch,
|
|
58
|
+
sha=response.diff_refs.head_sha,
|
|
59
|
+
),
|
|
60
|
+
target_branch=BranchRefSchema(
|
|
61
|
+
ref=response.target_branch,
|
|
62
|
+
sha=response.diff_refs.base_sha,
|
|
63
|
+
),
|
|
64
|
+
changed_files=[
|
|
65
|
+
change.new_path for change in response.changes if change.new_path
|
|
51
66
|
],
|
|
52
|
-
description=response.description,
|
|
53
|
-
source_branch=response.source_branch,
|
|
54
|
-
target_branch=response.target_branch,
|
|
55
|
-
changed_files=[change.new_path for change in response.changes if change.new_path],
|
|
56
67
|
)
|
|
57
68
|
except Exception as error:
|
|
58
69
|
logger.exception(
|
|
59
|
-
f"Failed to fetch MR info project_id={self.project_id} "
|
|
70
|
+
f"Failed to fetch MR info for project_id={self.project_id} "
|
|
60
71
|
f"merge_request_id={self.merge_request_id}: {error}"
|
|
61
72
|
)
|
|
62
|
-
return
|
|
73
|
+
return ReviewInfoSchema()
|
|
63
74
|
|
|
64
|
-
async def
|
|
75
|
+
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
65
76
|
try:
|
|
66
|
-
response = await self.http_client.mr.
|
|
77
|
+
response = await self.http_client.mr.get_notes(
|
|
67
78
|
project_id=self.project_id,
|
|
68
79
|
merge_request_id=self.merge_request_id,
|
|
69
80
|
)
|
|
70
81
|
logger.info(
|
|
71
|
-
f"Fetched comments for project_id={self.project_id}
|
|
82
|
+
f"Fetched general comments for project_id={self.project_id} "
|
|
83
|
+
f"merge_request_id={self.merge_request_id}"
|
|
72
84
|
)
|
|
73
85
|
|
|
74
|
-
return [
|
|
86
|
+
return [
|
|
87
|
+
ReviewCommentSchema(id=note.id, body=note.body or "")
|
|
88
|
+
for note in response.root
|
|
89
|
+
]
|
|
75
90
|
except Exception as error:
|
|
76
91
|
logger.exception(
|
|
77
|
-
f"Failed to fetch comments project_id={self.project_id} "
|
|
92
|
+
f"Failed to fetch general comments project_id={self.project_id} "
|
|
78
93
|
f"merge_request_id={self.merge_request_id}: {error}"
|
|
79
94
|
)
|
|
80
95
|
return []
|
|
81
96
|
|
|
82
|
-
async def
|
|
97
|
+
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
83
98
|
try:
|
|
84
99
|
response = await self.http_client.mr.get_discussions(
|
|
85
100
|
project_id=self.project_id,
|
|
86
101
|
merge_request_id=self.merge_request_id,
|
|
87
102
|
)
|
|
88
103
|
logger.info(
|
|
89
|
-
f"Fetched discussions for project_id={self.project_id}
|
|
104
|
+
f"Fetched inline discussions for project_id={self.project_id} "
|
|
105
|
+
f"merge_request_id={self.merge_request_id}"
|
|
90
106
|
)
|
|
91
107
|
|
|
92
108
|
return [
|
|
93
|
-
|
|
94
|
-
id=discussion.id,
|
|
95
|
-
notes=[MRNoteSchema(id=note.id, body=note.body or "") for note in discussion.notes],
|
|
96
|
-
)
|
|
109
|
+
ReviewCommentSchema(id=note.id, body=note.body or "")
|
|
97
110
|
for discussion in response.root
|
|
111
|
+
for note in discussion.notes
|
|
98
112
|
]
|
|
99
113
|
except Exception as error:
|
|
100
114
|
logger.exception(
|
|
101
|
-
f"Failed to fetch discussions project_id={self.project_id} "
|
|
115
|
+
f"Failed to fetch inline discussions project_id={self.project_id} "
|
|
102
116
|
f"merge_request_id={self.merge_request_id}: {error}"
|
|
103
117
|
)
|
|
104
118
|
return []
|
|
105
119
|
|
|
106
|
-
async def
|
|
120
|
+
async def create_general_comment(self, message: str) -> None:
|
|
107
121
|
try:
|
|
108
122
|
logger.info(
|
|
109
|
-
f"Posting comment to merge_request_id={self.merge_request_id}: {message}"
|
|
123
|
+
f"Posting general comment to merge_request_id={self.merge_request_id}: {message}"
|
|
110
124
|
)
|
|
111
|
-
await self.http_client.mr.
|
|
112
|
-
|
|
125
|
+
await self.http_client.mr.create_note(
|
|
126
|
+
body=message,
|
|
113
127
|
project_id=self.project_id,
|
|
114
128
|
merge_request_id=self.merge_request_id,
|
|
115
129
|
)
|
|
116
|
-
logger.info(f"Created comment in {self.merge_request_id
|
|
130
|
+
logger.info(f"Created general comment in merge_request_id={self.merge_request_id}")
|
|
117
131
|
except Exception as error:
|
|
118
|
-
logger.exception(
|
|
132
|
+
logger.exception(
|
|
133
|
+
f"Failed to create general comment in merge_request_id={self.merge_request_id}: {error}"
|
|
134
|
+
)
|
|
119
135
|
|
|
120
|
-
async def
|
|
136
|
+
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
121
137
|
try:
|
|
122
138
|
logger.info(
|
|
123
|
-
f"Posting
|
|
139
|
+
f"Posting inline comment in merge_request_id={self.merge_request_id} at {file}:{line}: {message}"
|
|
124
140
|
)
|
|
125
141
|
|
|
126
142
|
response = await self.http_client.mr.get_changes(
|
|
@@ -137,16 +153,18 @@ class GitLabVCSClient(VCSClient):
|
|
|
137
153
|
start_sha=response.diff_refs.start_sha,
|
|
138
154
|
new_path=file,
|
|
139
155
|
new_line=line,
|
|
140
|
-
)
|
|
156
|
+
),
|
|
141
157
|
)
|
|
142
158
|
await self.http_client.mr.create_discussion(
|
|
143
159
|
request=request,
|
|
144
160
|
project_id=self.project_id,
|
|
145
161
|
merge_request_id=self.merge_request_id,
|
|
146
162
|
)
|
|
147
|
-
logger.info(
|
|
163
|
+
logger.info(
|
|
164
|
+
f"Created inline comment in merge_request_id={self.merge_request_id} at {file}:{line}"
|
|
165
|
+
)
|
|
148
166
|
except Exception as error:
|
|
149
167
|
logger.exception(
|
|
150
|
-
f"Failed to create
|
|
168
|
+
f"Failed to create inline comment in merge_request_id={self.merge_request_id} "
|
|
151
169
|
f"at {file}:{line}: {error}"
|
|
152
170
|
)
|
ai_review/services/vcs/types.py
CHANGED
|
@@ -3,53 +3,62 @@ from typing import Protocol
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class
|
|
6
|
+
class UserSchema(BaseModel):
|
|
7
|
+
id: str | int | None = None
|
|
7
8
|
name: str = ""
|
|
8
9
|
username: str = ""
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class
|
|
12
|
+
class BranchRefSchema(BaseModel):
|
|
13
|
+
ref: str = ""
|
|
14
|
+
sha: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReviewInfoSchema(BaseModel):
|
|
18
|
+
id: str | int | None = None
|
|
12
19
|
title: str = ""
|
|
13
|
-
|
|
20
|
+
description: str = ""
|
|
21
|
+
author: UserSchema = Field(default_factory=UserSchema)
|
|
14
22
|
labels: list[str] = Field(default_factory=list)
|
|
23
|
+
assignees: list[UserSchema] = Field(default_factory=list)
|
|
24
|
+
reviewers: list[UserSchema] = Field(default_factory=list)
|
|
25
|
+
source_branch: BranchRefSchema = Field(default_factory=BranchRefSchema)
|
|
26
|
+
target_branch: BranchRefSchema = Field(default_factory=BranchRefSchema)
|
|
27
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
15
28
|
base_sha: str = ""
|
|
16
29
|
head_sha: str = ""
|
|
17
|
-
assignees: list[MRUserSchema] = Field(default_factory=list)
|
|
18
|
-
reviewers: list[MRUserSchema] = Field(default_factory=list)
|
|
19
30
|
start_sha: str = ""
|
|
20
|
-
description: str = ""
|
|
21
|
-
source_branch: str = ""
|
|
22
|
-
target_branch: str = ""
|
|
23
|
-
changed_files: list[str] = Field(default_factory=list)
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
class
|
|
27
|
-
id:
|
|
33
|
+
class ReviewCommentSchema(BaseModel):
|
|
34
|
+
id: str | int
|
|
28
35
|
body: str
|
|
36
|
+
file: str | None = None
|
|
37
|
+
line: int | None = None
|
|
29
38
|
|
|
30
39
|
|
|
31
|
-
class
|
|
32
|
-
id: str
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class MRCommentSchema(BaseModel):
|
|
37
|
-
id: int | str
|
|
38
|
-
body: str
|
|
40
|
+
class ReviewThreadSchema(BaseModel):
|
|
41
|
+
id: str | int
|
|
42
|
+
comments: list[ReviewCommentSchema]
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
class VCSClient(Protocol):
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
"""
|
|
47
|
+
Unified interface for version control system integrations (GitHub, GitLab, Bitbucket, etc.).
|
|
48
|
+
Designed for code review automation: fetching review info, comments, and posting feedback.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
async def get_review_info(self) -> ReviewInfoSchema:
|
|
52
|
+
"""Fetch general information about the current review (PR/MR)."""
|
|
44
53
|
|
|
45
|
-
async def
|
|
46
|
-
|
|
54
|
+
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
55
|
+
"""Fetch all top-level (non-inline) comments."""
|
|
47
56
|
|
|
48
|
-
async def
|
|
49
|
-
|
|
57
|
+
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
58
|
+
"""Fetch inline (file + line attached) comments."""
|
|
50
59
|
|
|
51
|
-
async def
|
|
52
|
-
|
|
60
|
+
async def create_general_comment(self, message: str) -> None:
|
|
61
|
+
"""Post a top-level comment."""
|
|
53
62
|
|
|
54
|
-
async def
|
|
55
|
-
|
|
63
|
+
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
64
|
+
"""Post a comment attached to a specific line in file."""
|
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.cost.schema import CostReportSchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ---------- tests: PERCENTAGE CALCULATIONS ----------
|
|
7
|
+
|
|
8
|
+
def test_percent_calculations() -> None:
|
|
9
|
+
"""
|
|
10
|
+
Should correctly calculate prompt and completion percent based on total cost.
|
|
11
|
+
"""
|
|
12
|
+
report = CostReportSchema(
|
|
13
|
+
model="gpt-4",
|
|
14
|
+
prompt_tokens=1000,
|
|
15
|
+
completion_tokens=500,
|
|
16
|
+
input_cost=0.02,
|
|
17
|
+
output_cost=0.03,
|
|
18
|
+
total_cost=0.05,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assert pytest.approx(report.prompt_percent, 0.1) == 40.0
|
|
22
|
+
assert pytest.approx(report.completion_percent, 0.1) == 60.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_percent_zero_total_cost() -> None:
|
|
26
|
+
"""
|
|
27
|
+
Should handle total_cost=0 without division errors and return 0%.
|
|
28
|
+
"""
|
|
29
|
+
report = CostReportSchema(
|
|
30
|
+
model="gpt-4o-mini",
|
|
31
|
+
prompt_tokens=100,
|
|
32
|
+
completion_tokens=50,
|
|
33
|
+
input_cost=0.01,
|
|
34
|
+
output_cost=0.01,
|
|
35
|
+
total_cost=0.0,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert report.prompt_percent == 0.0
|
|
39
|
+
assert report.completion_percent == 0.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------- tests: PRETTY LINES ----------
|
|
43
|
+
|
|
44
|
+
def test_pretty_prompt_line_format() -> None:
|
|
45
|
+
"""
|
|
46
|
+
Should render a formatted line for prompt tokens and cost.
|
|
47
|
+
"""
|
|
48
|
+
report = CostReportSchema(
|
|
49
|
+
model="gpt-4",
|
|
50
|
+
prompt_tokens=1234,
|
|
51
|
+
completion_tokens=0,
|
|
52
|
+
input_cost=0.012345,
|
|
53
|
+
output_cost=0.0,
|
|
54
|
+
total_cost=0.012345,
|
|
55
|
+
)
|
|
56
|
+
out = report.pretty_prompt_line
|
|
57
|
+
|
|
58
|
+
assert "- Prompt tokens:" in out
|
|
59
|
+
assert "1234" in out
|
|
60
|
+
assert "USD" in out
|
|
61
|
+
assert "(" in out
|
|
62
|
+
assert ")" in out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_pretty_completion_line_format() -> None:
|
|
66
|
+
"""
|
|
67
|
+
Should render a formatted line for completion tokens and cost.
|
|
68
|
+
"""
|
|
69
|
+
report = CostReportSchema(
|
|
70
|
+
model="gpt-4",
|
|
71
|
+
prompt_tokens=0,
|
|
72
|
+
completion_tokens=567,
|
|
73
|
+
input_cost=0.0,
|
|
74
|
+
output_cost=0.06789,
|
|
75
|
+
total_cost=0.06789,
|
|
76
|
+
)
|
|
77
|
+
out = report.pretty_completion_line
|
|
78
|
+
|
|
79
|
+
assert "- Completion tokens:" in out
|
|
80
|
+
assert "567" in out
|
|
81
|
+
assert "USD" in out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_pretty_total_line_format() -> None:
|
|
85
|
+
"""
|
|
86
|
+
Should render a formatted total cost line.
|
|
87
|
+
"""
|
|
88
|
+
report = CostReportSchema(
|
|
89
|
+
model="gpt-4",
|
|
90
|
+
prompt_tokens=0,
|
|
91
|
+
completion_tokens=0,
|
|
92
|
+
input_cost=0.0,
|
|
93
|
+
output_cost=0.0,
|
|
94
|
+
total_cost=1.234567,
|
|
95
|
+
)
|
|
96
|
+
out = report.pretty_total_line
|
|
97
|
+
|
|
98
|
+
assert "- Total:" in out
|
|
99
|
+
assert "USD" in out
|
|
100
|
+
assert "1.234567" in out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------- tests: MULTILINE OUTPUT ----------
|
|
104
|
+
|
|
105
|
+
def test_pretty_multiline_output() -> None:
|
|
106
|
+
"""
|
|
107
|
+
Should produce a full formatted cost report with all parts.
|
|
108
|
+
"""
|
|
109
|
+
report = CostReportSchema(
|
|
110
|
+
model="gpt-4o-mini",
|
|
111
|
+
prompt_tokens=200,
|
|
112
|
+
completion_tokens=100,
|
|
113
|
+
input_cost=0.002,
|
|
114
|
+
output_cost=0.001,
|
|
115
|
+
total_cost=0.003,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
out = report.pretty()
|
|
119
|
+
|
|
120
|
+
assert out.startswith("\n💰 Estimated Cost for `gpt-4o-mini`")
|
|
121
|
+
assert "- Prompt tokens:" in out
|
|
122
|
+
assert "- Completion tokens:" in out
|
|
123
|
+
assert "- Total:" in out
|
|
124
|
+
assert out.count("\n") >= 4
|