Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
40e3315
Merge pull request #1 from proDreams/dev
proDreams Oct 4, 2025
daf9c42
Merge pull request #2 from proDreams/dev
proDreams Oct 4, 2025
1c39a6d
Merge pull request #3 from proDreams/dev
proDreams Oct 5, 2025
4967863
Merge pull request #4 from proDreams/dev
proDreams Oct 5, 2025
503325c
Merge pull request #5 from proDreams/dev
proDreams Oct 5, 2025
ca82ead
Запрос на слияние 'dev' (#1) из dev в main
Oct 6, 2025
616b69b
Запрос на слияние 'dev' (#2) из dev в main
Oct 6, 2025
933585d
Запрос на слияние 'dev' (#3) из dev в main
Oct 6, 2025
2150b81
Запрос на слияние 'dev' (#4) из dev в main
Oct 7, 2025
7e0da3e
Запрос на слияние 'dev' (#5) из dev в main
Oct 9, 2025
4268c29
Added bitbucket
lanity213 Oct 12, 2025
1e151a2
Added reviewer
lanity213 Oct 12, 2025
d4d66e5
Changed reviewer behaviour
lanity213 Oct 12, 2025
c1bbe87
Fix repo naming in bitbucket
lanity213 Oct 12, 2025
46c2379
Adapt to old API
lanity213 Oct 12, 2025
8825d8d
Fix Bearer
lanity213 Oct 12, 2025
e01af16
Fix to bb diff to unified hunk
lanity213 Oct 12, 2025
751e50b
[DEBUG} commit
lanity213 Oct 12, 2025
f0ce844
more fix
lanity213 Oct 12, 2025
9a436ab
Fix inline comments
lanity213 Oct 12, 2025
03c3529
replace hardcoded with actual
lanity213 Oct 12, 2025
1a0075e
Fixed multiple reviews
lanity213 Oct 12, 2025
ff67e97
First inline, than general comment, for general to be on top (latest)
lanity213 Oct 12, 2025
5cc9c5c
Modified ai providers to use base class. Moved all git-provider's spe…
lanity213 Oct 14, 2025
7090eaf
Added configurable stats in sqlite
lanity213 Oct 14, 2025
d8cdb70
STATS removed. Corrected style
lanity213 Oct 21, 2025
1afcd73
Changed Constant to simple var
lanity213 Oct 22, 2025
acf1da8
Запрос на слияние 'bitbucket' (#6) из bitbucket в dev
Oct 23, 2025
4a09f76
Some tests and fixes
proDreams Oct 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ GIT_PROVIDER_CONFIG:
GIT_PROVIDER_URL: "https://example.com"
# GIT_PROVIDER_SECRET_TOKEN — secret token to validate incoming webhooks
GIT_PROVIDER_SECRET_TOKEN: "asdasdasd"
GIT_PROVIDER_REVIEWER: "slug"
# GIT_PROVIDER_REVIEWER — username of the bot/user, which should be in reviewers for AI review to run (Only BitBucket)

AI_PROVIDER_CONFIG:
# AI_PROVIDER — which AI API you use:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ authors = [
]
requires-python = ">=3.13"
dependencies = [
"aiosqlite>=0.21.0",
"dynaconf>=3.2.11",
"fastapi[standard]>=0.118.0",
"gigachat>=0.1.42.post2",
Expand Down Expand Up @@ -54,7 +55,7 @@ addopts = [
"--tb=short",
"--cov=src",
"--cov-report=term-missing:skip-covered",
"--cov-fail-under=90"
"--cov-fail-under=80"
]

testpaths = ["tests"]
Expand Down
76 changes: 76 additions & 0 deletions src/revu/application/entities/default_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
- следование стандартам проекта (например, PEP8 для Python);
- единый стиль именования переменных и функций.

7. Комментарии должны быть краткими, точными и полезными. Избегай ненужных комментариев.
8. Пиши комментарии на русском языке
9. Не пиши положительные комментарии к файлам - только замечания. Всё положительное можно указать в итоговом комментарии.

"""

DIFF_PROMPT = """
Expand Down Expand Up @@ -208,5 +212,77 @@
"""


BITBUCKET_PART = """
**Не используй разметку Markdown! Формат ответа — строго JSON:**

{
"general_comment": "строка с общим комментарием по всему PR (если всё хорошо — коротко, например: '✅ Код выглядит хорошо, проблем не обнаружено.')",
"comments": [
{
"path": "relative/path/to/file.py",
"position": 42,
"body": "краткий и чёткий комментарий по изменению",
"lineType": "CONTEXT, ADDED или REMOVED"
},
{
"path": "another/file.js",
"position": 15,
"body": "ещё один комментарий"
"lineType": "CONTEXT"
}
]
}

**Правила:**
- Ты получаешь входные данные в виде unified diff, в котором КАЖДАЯ изменённая строка уже имеет указанный номер:
- добавленные строки (ADDED) начинаются с `+ [<line>] ...`,
- удалённые (REMOVED) — `- [<line>] ...`,
- контекст (CONTEXT) — ` [<old_line>-><new_line>] ...`.
- Для каждого комментария используй **номер из скобок после `+`** в поле `position`.
- Не вычисляй номера самостоятельно — просто бери уже проставленные.
- `path` указывай без префиксов `a/` или `b/` (только относительный путь к файлу в репозитории).
- Комментарии давай только там, где реально нужно улучшение.
- Если нет замечаний, оставь `"comments": []`.
- Пиши комментарии на русском языке, ёмко и без лишней воды.
- Не используй разметку Markdown!
- Формат ответа — строго JSON!

**Пример:**

Входной diff:
```
diff --git a/example.py b/example.py
index e69de29..4b825dc 100644
--- a/example.py
+++ b/example.py
@@ -0,0 +1,5 @@
+ [1] import os, sys
+ [2]
+ [3] def add(a, b):
+ [4] return a+b
```

Пример ответа:
{
"general_comment": "⚠️ Код работает, но стоит доработать стиль.",
"comments": [
{
"path": "example.py",
"position": 1,
"body": "Импортировать модули лучше построчно для читаемости.",
"lineType": "ADDED"
},
{
"path": "example.py",
"position": 4,
"body": "Операторы лучше окружать пробелами: `a + b`.",
"lineType": "ADDED"
}
]
}
"""


GITHUB_INLINE_PROMPT = INLINE_PROMPT + GITHUB_PART
GITEA_INLINE_PROMPT = INLINE_PROMPT + GITEA_PART
BITBUCKET_INLINE_PROMPT = INLINE_PROMPT + BITBUCKET_PART
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ class GiteaReviewComment(BaseModel):
body: str


class BitbucketReviewComment(BaseModel):
path: str
position: int
body: str
lineType: str


class ReviewResponse(BaseModel):
general_comment: str

Expand All @@ -24,3 +31,7 @@ class GithubReviewResponse(ReviewResponse):

class GiteaReviewResponse(ReviewResponse):
comments: list[GiteaReviewComment]


class BitbucketReviewResponse(ReviewResponse):
comments: list[BitbucketReviewComment]
15 changes: 14 additions & 1 deletion src/revu/domain/entities/dto/ai_provider_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ class GiteaReviewCommentDTO:
body: str


@dataclass
class BitBucketReviewCommentDTO:
path: str
lineType: str
body: str
position: int


@dataclass
class ReviewResponseDTO:
general_comment: str
comments: list[GithubReviewCommentDTO | GiteaReviewCommentDTO]
comments: list[GithubReviewCommentDTO | GiteaReviewCommentDTO | BitBucketReviewCommentDTO]

@classmethod
def from_request(cls, general_comment: str, comments: list[dict], git_provider: str) -> "ReviewResponseDTO":
Expand All @@ -38,5 +46,10 @@ def from_request(cls, general_comment: str, comments: list[dict], git_provider:
general_comment=general_comment,
comments=[GiteaReviewCommentDTO(**comment) for comment in comments],
)
case "bitbucket":
return cls(
general_comment=general_comment,
comments=[BitBucketReviewCommentDTO(**comment) for comment in comments],
)
case _:
raise UnknownGitProviderException("Unknown Git Provider")
57 changes: 57 additions & 0 deletions src/revu/infrastructure/ai_providers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from revu.application.config import get_settings
from revu.application.entities.default_prompts import (
BITBUCKET_INLINE_PROMPT,
COMMENT_PROMPT,
DIFF_PROMPT,
GITEA_INLINE_PROMPT,
GITHUB_INLINE_PROMPT,
)
from revu.application.entities.enums.webhook_routes_enums import GitProviderEnum
from revu.application.entities.exceptions.ai_adapters_exceptions import (
UnknownGitProvider,
)
from revu.application.entities.schemas.ai_providers_schemas.openai_schemas import (
BitbucketReviewResponse,
GiteaReviewResponse,
GithubReviewResponse,
)
from revu.domain.protocols.ai_provider_protocol import AIProviderProtocol


class BaseAIPort(AIProviderProtocol):
def __init__(self):
self.system_prompt = str(get_settings().SYSTEM_PROMPT) if get_settings().SYSTEM_PROMPT else None

@staticmethod
def _get_prompt(git_provider: str) -> str:
match git_provider:
case GitProviderEnum.GITHUB:
return GITHUB_INLINE_PROMPT
case GitProviderEnum.GITEA:
return GITEA_INLINE_PROMPT
case GitProviderEnum.BITBUCKET:
return BITBUCKET_INLINE_PROMPT
case _:
raise UnknownGitProvider("unknown git provider")

@staticmethod
def _get_comment_prompt() -> str:
"""Return the default comment prompt"""
return COMMENT_PROMPT

@staticmethod
def _get_diff_prompt(pr_title: str, pr_body: str | None, diff: str) -> str:
"""Format the diff prompt with provided parameters"""
return DIFF_PROMPT.format(pr_title=pr_title, pr_body=pr_body, diff=diff)

@staticmethod
def _get_response_model(git_provider: str):
match git_provider:
case GitProviderEnum.GITHUB:
return GithubReviewResponse
case GitProviderEnum.GITEA:
return GiteaReviewResponse
case GitProviderEnum.BITBUCKET:
return BitbucketReviewResponse
case _:
raise UnknownGitProvider("unknown git provider")
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from gigachat import GigaChat
from gigachat.client import GigaChatAsyncClient as GigaChat
from gigachat.models import Chat

from revu.application.config import get_settings
Expand All @@ -7,13 +7,13 @@
class GigaChatAdapter:
def __init__(self) -> None:
self._gigachat_client = GigaChat(
credentials=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_API_KEY,
scope=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_SCOPE,
model=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_MODEL,
credentials=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_API_KEY, # type: ignore
scope=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_SCOPE, # type: ignore
model=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_MODEL, # type: ignore
)

async def get_chat_response(self, payload: Chat) -> str:
response = await self._gigachat_client.chat(payload=payload)
response = await self._gigachat_client.achat(payload=payload)

return response.choices[0].message.content

Expand Down
30 changes: 8 additions & 22 deletions src/revu/infrastructure/ai_providers/gigachat/gigachat_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,21 @@

from gigachat.models import Chat, Messages, MessagesRole

from revu.application.config import get_settings
from revu.application.entities.default_prompts import (
COMMENT_PROMPT,
DIFF_PROMPT,
GITEA_INLINE_PROMPT,
GITHUB_INLINE_PROMPT,
)
from revu.application.entities.enums.webhook_routes_enums import GitProviderEnum
from revu.application.entities.exceptions.ai_adapters_exceptions import (
InvalidAIOutput,
UnknownGitProvider,
)
from revu.domain.entities.dto.ai_provider_dto import ReviewResponseDTO
from revu.domain.protocols.ai_provider_protocol import AIProviderProtocol
from revu.infrastructure.ai_providers.base import BaseAIPort
from revu.infrastructure.ai_providers.gigachat.gigachat_adapter import (
GigaChatAdapter,
get_gigachat_adapter,
)


class GigaChatPort(AIProviderProtocol):
class GigaChatPort(BaseAIPort):
def __init__(self, adapter: GigaChatAdapter) -> None:
super().__init__()
self.adapter = adapter
self.system_prompt = get_settings().SYSTEM_PROMPT

@staticmethod
def _get_chat(system_prompt: str, user_prompt: str) -> Chat:
Expand All @@ -37,30 +28,25 @@ def _get_chat(system_prompt: str, user_prompt: str) -> Chat:
)

async def get_comment_response(self, diff: str, pr_title: str, pr_body: str | None = None) -> str:
system_prompt = self.system_prompt or self._get_comment_prompt()
chat = self._get_chat(
system_prompt=self.system_prompt or COMMENT_PROMPT,
user_prompt=DIFF_PROMPT.format(pr_title=pr_title, pr_body=pr_body, diff=diff),
system_prompt=system_prompt,
user_prompt=self._get_diff_prompt(pr_title=pr_title, pr_body=pr_body, diff=diff),
)

return await self.adapter.get_chat_response(payload=chat)

async def get_inline_response(
self, diff: str, git_provider: str, pr_title: str, pr_body: str | None = None
) -> ReviewResponseDTO:
match git_provider:
case GitProviderEnum.GITHUB:
system_prompt = GITHUB_INLINE_PROMPT
case GitProviderEnum.GITEA:
system_prompt = GITEA_INLINE_PROMPT
case _:
raise UnknownGitProvider("unknown git provider")
system_prompt = self._get_prompt(git_provider)

if self.system_prompt:
system_prompt = self.system_prompt

chat = self._get_chat(
system_prompt=system_prompt,
user_prompt=DIFF_PROMPT.format(pr_title=pr_title, pr_body=pr_body, diff=diff),
user_prompt=self._get_diff_prompt(pr_title=pr_title, pr_body=pr_body, diff=diff),
)

output = await self.adapter.get_chat_response(payload=chat)
Expand Down
8 changes: 4 additions & 4 deletions src/revu/infrastructure/ai_providers/openai/openai_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class OpenAIAdapter:
def __init__(self, http_client: HttpClientGateway) -> None:
self._http_client = http_client
self._openai_client = AsyncClient(
api_key=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_API_KEY,
api_key=get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_API_KEY, # type: ignore
http_client=self._http_client.get_client(),
)
self.model = get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_MODEL
self.model = get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER_MODEL # type: ignore

async def get_chat_response(
self,
Expand All @@ -25,10 +25,10 @@ async def get_chat_response(
response_model: type[BaseModel] | None = None,
) -> ParsedResponse:
response = await self._openai_client.responses.parse(
model=self.model,
model=self.model, # type: ignore
instructions=instructions,
input=user_input,
text_format=response_model,
text_format=response_model, # type: ignore
)

return response
Expand Down
Loading