Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ReVu — это self-hosted вебхук-сервис для автоматич
* **Инлайн (`inline`)** — точечные комментарии к конкретным изменениям в коде.
* **Гибкая интеграция**
* Поддержка **GitHub** и **Gitea** (расширение до GitLab, Bitbucket и др. в планах).
* Совместимость с **OpenAI API** и его альтернативами (OpenRouter, LocalAI и др.).
* Совместимость с **OpenAI API** и его альтернативами (OpenRouter, LocalAI и др.), также Российскими ИИ-провайдерами: GigaChat и YandexGPT.
* **Полностью self-hosted**
* Разворачивается на ваших серверах без передачи данных третьим лицам.
* Можно использовать локальные модели (например, DeepSeek, Qwen), запуская их отдельно в вашей инфраструктуре.
Expand Down Expand Up @@ -77,7 +77,7 @@ ReVu — это self-hosted вебхук-сервис для автоматич
- [ ] Anthropic
- [ ] Qween
- [x] GigaChat
- [ ] YandexGPT
- [x] YandexGPT
- [ ] Гибкая настройка кастомных промптов для ревью
- [ ] Подробная документация по конфигурации и запуску
- [ ] Англоязычная версия README
Expand Down Expand Up @@ -141,10 +141,13 @@ ReVu — это self-hosted вебхук-сервис для автоматич
- `AI_PROVIDER` - используемый ИИ-провайдер. Доступные варианты:
- `"openai"` - официальный API от OpenAI
- `"openai_compatible"` - совместимый с OpenAI провайдер
- `"gigachat"` - официальный GigaChat
- `"yandexgpt"` - официальный YandexGPT
- `AI_PROVIDER_API_KEY` - API Ключ от ИИ-провайдера
- (необязательно) `AI_PROVIDER_BASE_URL` - URL от OpenAI-совместимого провайдера
- `AI_PROVIDER_MODEL` - используемая модель (например, `gpt-4o-mini`)
- (необязательно) `AI_PROVIDER_SCOPE` - только для GigaChat. Определяет используемую версию GigaChat, указан в личном кабинете.
- (необязательно) `AI_PROVIDER_FOLDER_ID` - только для YandexGPT. Идентификатор рабочей директории, указан в личном кабинете.
- `LOG_LEVEL` - уровень логирования (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`)
4. Сохраните и выйдите, нажав CTRL+S, затем CTRL+X.
3. Скачайте docker-compose-файл:
Expand Down
2 changes: 2 additions & 0 deletions config/settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ AI_PROVIDER_CONFIG:
AI_PROVIDER_BASE_URL: "https://example.com/v1"
# AI_PROVIDER_SCOPE — required only for GigaChat API (scope parameter used for authorization)
AI_PROVIDER_SCOPE: "GIGACHAT_API_PERS"
# AI_PROVIDER_FOLDER_ID - required only for YandexGPT. Project folder identification
AI_PROVIDER_FOLDER_ID: "abcdef"

# logger
# LOG_LEVEL — log detail level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
Expand Down
2 changes: 2 additions & 0 deletions config/settings.yaml.example_full
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ AI_PROVIDER_CONFIG:
AI_PROVIDER_BASE_URL: "https://example.com/v1"
# AI_PROVIDER_SCOPE — required only for GigaChat API (scope parameter used for authorization)
AI_PROVIDER_SCOPE: "GIGACHAT_API_PERS"
# AI_PROVIDER_FOLDER_ID - required only for YandexGPT. Project folder identification
AI_PROVIDER_FOLDER_ID: "abcdef"

# HTTP_CLIENT_TIMEOUT — how long to wait (in seconds) before HTTP requests fail
HTTP_CLIENT_TIMEOUT: 30.0
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "revu"
version = "1.2.0"
version = "1.3.0"
description = "Self-hosted AI code review for your Pull Requests."
authors = [
{ name = "proDream", email = "sushkoos@gmail.com" }
Expand All @@ -14,6 +14,7 @@ dependencies = [
"httpx>=0.28.1",
"openai>=2.0.1",
"unidiff>=0.7.5",
"yandex-cloud-ml-sdk>=0.16.0",
]

[dependency-groups]
Expand Down
8 changes: 6 additions & 2 deletions src/revu/application/entities/default_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"""

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

{
"general_comment": "строка с общим комментарием по всему PR (если всё хорошо — коротко, например: '✅ Код выглядит хорошо, проблем не обнаружено.')",
Expand Down Expand Up @@ -100,6 +100,8 @@
- Комментарии давай только там, где реально нужно улучшение.
- Если нет замечаний, оставь `"comments": []`.
- Пиши комментарии на русском языке, ёмко и без лишней воды.
- Не используй разметку Markdown!
- Формат ответа — строго JSON!

**Пример:**

Expand Down Expand Up @@ -135,7 +137,7 @@
"""

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

{
"general_comment": "строка с общим комментарием по всему PR (если всё хорошо — коротко, например: '✅ Код выглядит хорошо, проблем не обнаружено.')",
Expand Down Expand Up @@ -167,6 +169,8 @@
- Комментарии давай только там, где реально нужно улучшение.
- Если нет замечаний, оставь `"comments": []`.
- Пиши комментарии на русском языке, ёмко и без лишней воды.
- Не используй разметку Markdown!
- Формат ответа — строго JSON!

**Пример:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ class AIProviderEnum(StrEnum):
OPENAI = "openai"
OPENAI_COMPATIBLE = "openai_compatible"
GIGACHAT = "gigachat"
YANDEXGPT = "yandexgpt"
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ class UnknownGitProvider(CoreException):

class InvalidAIOutput(CoreException):
pass


class NoAIResponse(CoreException):
pass
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from yandex_cloud_ml_sdk import AsyncYCloudML

from revu.application.config import get_settings
from revu.application.entities.exceptions.ai_adapters_exceptions import NoAIResponse


class YandexGPTAdapter:
def __init__(self) -> None:
self._ai_provider_config = get_settings().AI_PROVIDER_CONFIG
self._yandexgpt_client = AsyncYCloudML(
folder_id=self._ai_provider_config.AI_PROVIDER_FOLDER_ID, auth=self._ai_provider_config.AI_PROVIDER_API_KEY
)

async def get_chat_response(self, messages: list[dict[str, str]]) -> str:
model = self._yandexgpt_client.models.completions(self._ai_provider_config.AI_PROVIDER_MODEL)

response = await model.run(messages)

if response:
return response[0].text
else:
raise NoAIResponse("Yandex GPT returned no response")


def get_yandexgpt_adapter() -> YandexGPTAdapter:
return YandexGPTAdapter()
78 changes: 78 additions & 0 deletions src/revu/infrastructure/ai_providers/yandexgpt/yandexgpt_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import json

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.yandexgpt.yandexgpt_adapter import (
YandexGPTAdapter,
get_yandexgpt_adapter,
)


class YandexGPTPort(AIProviderProtocol):
def __init__(self, adapter: YandexGPTAdapter) -> None:
self.adapter = adapter
self.system_prompt = get_settings().SYSTEM_PROMPT

@staticmethod
def _get_messages(system_prompt: str, user_prompt: str) -> list[dict[str, str]]:
return [{"role": "system", "text": system_prompt}, {"role": "user", "text": user_prompt}]

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

return await self.adapter.get_chat_response(messages=messages)

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")

if self.system_prompt:
system_prompt = self.system_prompt

messages = self._get_messages(
system_prompt=system_prompt,
user_prompt=DIFF_PROMPT.format(pr_title=pr_title, pr_body=pr_body, diff=diff),
)

output = await self.adapter.get_chat_response(messages=messages)

# temporary solution
if output.startswith("```") and output.endswith("```"):
output = output[3:-3]

try:
serialized_output = json.loads(output)
except json.decoder.JSONDecodeError:
raise InvalidAIOutput("invalid JSON response from yandexgpt")

return ReviewResponseDTO.from_request(
general_comment=serialized_output["general_comment"],
comments=serialized_output["comments"],
git_provider=git_provider,
)


def get_yandexgpt_port() -> YandexGPTPort:
return YandexGPTPort(adapter=get_yandexgpt_adapter())
8 changes: 7 additions & 1 deletion src/revu/presentation/webhooks/di.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
OpenAICompatiblePort,
get_openai_compatible_port,
)
from revu.infrastructure.ai_providers.yandexgpt.yandexgpt_port import (
YandexGPTPort,
get_yandexgpt_port,
)
from revu.infrastructure.git_providers.gitea.gitea_port import (
GiteaPort,
get_gitea_port,
Expand All @@ -40,14 +44,16 @@ def get_git_provider_port() -> GithubPort | GiteaPort:
raise GitProviderException("Unknown GIT provider")


def get_ai_provider_port() -> OpenAIPort | OpenAICompatiblePort | GigaChatPort:
def get_ai_provider_port() -> OpenAIPort | OpenAICompatiblePort | GigaChatPort | YandexGPTPort:
match get_settings().AI_PROVIDER_CONFIG.AI_PROVIDER:
case AIProviderEnum.OPENAI:
return get_openai_port()
case AIProviderEnum.OPENAI_COMPATIBLE:
return get_openai_compatible_port()
case AIProviderEnum.GIGACHAT:
return get_gigachat_port()
case AIProviderEnum.YANDEXGPT:
return get_yandexgpt_port()
case _:
raise AIProviderException("Unknown AI provider")

Expand Down
Loading