feat: добавлен функционал для извлечения и отправки описания PR в Telegram

- Реализована возможность получения тела последнего объединенного PR по коммиту в GitHub Actions.
- Добавлен шаг для отправки описания PR в важные логи через Telegram.
- Обновлены тесты для проверки нового функционала и улучшения логики обработки сообщений.
This commit is contained in:
2026-01-26 22:40:05 +03:00
parent feee7f010c
commit be8af704ba
10 changed files with 100 additions and 47 deletions

View File

@@ -173,6 +173,56 @@ jobs:
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
continue-on-error: true
- name: Get PR body from merged PR
if: job.status == 'success' && github.event_name == 'push'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "🔍 Searching for merged PR associated with commit ${{ github.sha }}..."
# Находим последний мерженный PR для main ветки по merge commit SHA
COMMIT_SHA="${{ github.sha }}"
PR_NUMBER=$(gh pr list --state merged --base main --limit 10 --json number,mergeCommit --jq ".[] | select(.mergeCommit.oid == \"$COMMIT_SHA\") | .number" | head -1)
# Если не нашли по merge commit, ищем последний мерженный PR
if [ -z "$PR_NUMBER" ]; then
echo "⚠️ PR not found by merge commit, trying to get latest merged PR..."
PR_NUMBER=$(gh pr list --state merged --base main --limit 1 --json number --jq '.[0].number')
fi
if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then
echo "✅ Found PR #$PR_NUMBER"
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq '.body // ""')
if [ -n "$PR_BODY" ] && [ "$PR_BODY" != "null" ]; then
echo "PR_BODY<<EOF" >> $GITHUB_ENV
echo "$PR_BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
echo "✅ PR body extracted successfully"
else
echo "⚠️ PR body is empty"
fi
else
echo "⚠️ No merged PR found for this commit"
fi
continue-on-error: true
- name: Send PR body to important logs
if: job.status == 'success' && github.event_name == 'push' && env.PR_BODY != ''
uses: appleboy/telegram-action@v1.0.0
with:
to: ${{ secrets.IMPORTANT_LOGS_CHAT }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
📋 Pull Request Description (PR #${{ env.PR_NUMBER }}):
${{ env.PR_BODY }}
🔗 PR: ${{ github.server_url }}/${{ github.repository }}/pull/${{ env.PR_NUMBER }}
📝 Commit: ${{ github.sha }}
continue-on-error: true
rollback:
runs-on: ubuntu-latest
name: Rollback to Previous Version

View File

@@ -1,6 +1,5 @@
"""Репозиторий для работы с миграциями базы данных."""
import aiosqlite
from database.base import DatabaseConnection

View File

@@ -7,17 +7,12 @@
- ScoringManager - объединение всех сервисов скоринга
"""
from .base import ScoringResult, ScoringServiceProtocol, CombinedScore
from .exceptions import (
ScoringError,
ModelNotLoadedError,
VectorStoreError,
DeepSeekAPIError,
InsufficientExamplesError,
TextTooShortError,
)
from .rag_client import RagApiClient
from .base import CombinedScore, ScoringResult, ScoringServiceProtocol
from .deepseek_service import DeepSeekService
from .exceptions import (DeepSeekAPIError, InsufficientExamplesError,
ModelNotLoadedError, ScoringError, TextTooShortError,
VectorStoreError)
from .rag_client import RagApiClient
from .scoring_manager import ScoringManager
__all__ = [

View File

@@ -3,8 +3,8 @@
"""
from dataclasses import dataclass, field
from typing import Optional, Protocol, Dict, Any
from datetime import datetime
from typing import Any, Dict, Optional, Protocol
@dataclass

View File

@@ -6,12 +6,11 @@ DeepSeek API сервис для скоринга постов.
import asyncio
import json
from typing import Optional, List
from typing import List, Optional
import httpx
from helper_bot.utils.metrics import track_errors, track_time
from logs.custom_logger import logger
from helper_bot.utils.metrics import track_time, track_errors
from .base import ScoringResult
from .exceptions import DeepSeekAPIError, ScoringError, TextTooShortError

View File

@@ -4,13 +4,15 @@ HTTP клиент для взаимодействия с внешним RAG се
Использует REST API для получения скоров и отправки примеров.
"""
from typing import Optional, Dict, Any
from typing import Any, Dict, Optional
import httpx
from helper_bot.utils.metrics import track_errors, track_time
from logs.custom_logger import logger
from helper_bot.utils.metrics import track_time, track_errors
from .base import ScoringResult
from .exceptions import ScoringError, InsufficientExamplesError, TextTooShortError
from .exceptions import (InsufficientExamplesError, ScoringError,
TextTooShortError)
class RagApiClient:

View File

@@ -8,13 +8,14 @@
import asyncio
from typing import Optional
from helper_bot.utils.metrics import track_errors, track_time
from logs.custom_logger import logger
from helper_bot.utils.metrics import track_time, track_errors
from .base import CombinedScore, ScoringResult
from .rag_client import RagApiClient
from .deepseek_service import DeepSeekService
from .exceptions import ScoringError, InsufficientExamplesError, TextTooShortError
from .exceptions import (InsufficientExamplesError, ScoringError,
TextTooShortError)
from .rag_client import RagApiClient
class ScoringManager:

View File

@@ -130,11 +130,8 @@ class BaseDependencyFactory:
Вызывается лениво при первом обращении к get_scoring_manager().
"""
from helper_bot.services.scoring import (
ScoringManager,
RagApiClient,
DeepSeekService,
)
from helper_bot.services.scoring import (DeepSeekService, RagApiClient,
ScoringManager)
scoring_config = self.settings['Scoring']

View File

@@ -111,7 +111,7 @@ class TestKeyboards:
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) == 2 # Две строки
assert len(keyboard.keyboard) == 3 # Три строки
# Проверяем первую строку (3 кнопки)
first_row = keyboard.keyboard[0]
@@ -124,7 +124,12 @@ class TestKeyboards:
second_row = keyboard.keyboard[1]
assert len(second_row) == 2
assert second_row[0].text == "Разбан (список)"
assert second_row[1].text == "Вернуться в бота"
assert second_row[1].text == "📊 ML Статистика"
# Проверяем третью строку (1 кнопка)
third_row = keyboard.keyboard[2]
assert len(third_row) == 1
assert third_row[0].text == "Вернуться в бота"
def test_get_reply_keyboard_for_post(self):
"""Тест клавиатуры для постов"""

View File

@@ -3,16 +3,14 @@
"""
import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Импорты для тестирования базовых классов
from helper_bot.services.scoring.base import ScoringResult, CombinedScore
from helper_bot.services.scoring.exceptions import (
ScoringError,
InsufficientExamplesError,
TextTooShortError,
)
from helper_bot.services.scoring.base import CombinedScore, ScoringResult
from helper_bot.services.scoring.exceptions import (InsufficientExamplesError,
ScoringError,
TextTooShortError)
class TestScoringResult:
@@ -215,7 +213,8 @@ class TestDeepSeekService:
@pytest.fixture
def deepseek_service(self):
"""Создает DeepSeekService для тестов."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService
from helper_bot.services.scoring.deepseek_service import \
DeepSeekService
return DeepSeekService(
api_key="test_key",
enabled=True,
@@ -224,7 +223,8 @@ class TestDeepSeekService:
def test_service_disabled_without_key(self):
"""Тест отключения сервиса без API ключа."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService
from helper_bot.services.scoring.deepseek_service import \
DeepSeekService
service = DeepSeekService(api_key=None, enabled=True)
assert service.is_enabled is False
@@ -255,7 +255,8 @@ class TestDeepSeekService:
@pytest.mark.asyncio
async def test_calculate_score_disabled(self):
"""Тест расчета скора при отключенном сервисе."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService
from helper_bot.services.scoring.deepseek_service import \
DeepSeekService
service = DeepSeekService(api_key=None, enabled=False)
with pytest.raises(ScoringError):
@@ -281,6 +282,8 @@ class TestScoringManager:
source="rag",
model="rubert",
))
mock.add_positive_example = AsyncMock()
mock.add_negative_example = AsyncMock()
return mock
@pytest.fixture
@@ -293,6 +296,8 @@ class TestScoringManager:
source="deepseek",
model="deepseek-chat",
))
mock.add_positive_example = AsyncMock()
mock.add_negative_example = AsyncMock()
return mock
@pytest.mark.asyncio
@@ -301,7 +306,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_service=mock_rag_service,
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
@@ -317,7 +322,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_service=mock_rag_service,
rag_client=mock_rag_service,
deepseek_service=None,
)
@@ -331,7 +336,7 @@ class TestScoringManager:
"""Тест скоринга пустого текста."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(rag_service=mock_rag_service)
manager = ScoringManager(rag_client=mock_rag_service)
result = await manager.score_post("")
@@ -347,7 +352,7 @@ class TestScoringManager:
mock_rag_service.calculate_score = AsyncMock(side_effect=Exception("Test error"))
manager = ScoringManager(
rag_service=mock_rag_service,
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
@@ -365,7 +370,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_service=mock_rag_service,
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
@@ -380,7 +385,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_service=mock_rag_service,
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)