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

@@ -172,6 +172,56 @@ jobs:
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
continue-on-error: true 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: rollback:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,16 +3,14 @@
""" """
import json import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Импорты для тестирования базовых классов # Импорты для тестирования базовых классов
from helper_bot.services.scoring.base import ScoringResult, CombinedScore from helper_bot.services.scoring.base import CombinedScore, ScoringResult
from helper_bot.services.scoring.exceptions import ( from helper_bot.services.scoring.exceptions import (InsufficientExamplesError,
ScoringError, ScoringError,
InsufficientExamplesError, TextTooShortError)
TextTooShortError,
)
class TestScoringResult: class TestScoringResult:
@@ -159,7 +157,7 @@ class TestVectorStore:
def test_max_examples_limit(self, vector_store): def test_max_examples_limit(self, vector_store):
"""Тест ограничения максимального количества примеров.""" """Тест ограничения максимального количества примеров."""
import numpy as np import numpy as np
# Добавляем больше чем max_examples # Добавляем больше чем max_examples
for i in range(150): for i in range(150):
vector = np.random.randn(768).astype(np.float32) vector = np.random.randn(768).astype(np.float32)
@@ -179,7 +177,7 @@ class TestVectorStore:
def test_calculate_similarity_with_examples(self, vector_store): def test_calculate_similarity_with_examples(self, vector_store):
"""Тест расчета скора с примерами.""" """Тест расчета скора с примерами."""
import numpy as np import numpy as np
# Добавляем положительные примеры # Добавляем положительные примеры
for i in range(10): for i in range(10):
vector = np.random.randn(768).astype(np.float32) vector = np.random.randn(768).astype(np.float32)
@@ -215,7 +213,8 @@ class TestDeepSeekService:
@pytest.fixture @pytest.fixture
def deepseek_service(self): def deepseek_service(self):
"""Создает DeepSeekService для тестов.""" """Создает DeepSeekService для тестов."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService from helper_bot.services.scoring.deepseek_service import \
DeepSeekService
return DeepSeekService( return DeepSeekService(
api_key="test_key", api_key="test_key",
enabled=True, enabled=True,
@@ -224,7 +223,8 @@ class TestDeepSeekService:
def test_service_disabled_without_key(self): def test_service_disabled_without_key(self):
"""Тест отключения сервиса без API ключа.""" """Тест отключения сервиса без 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) service = DeepSeekService(api_key=None, enabled=True)
assert service.is_enabled is False assert service.is_enabled is False
@@ -255,7 +255,8 @@ class TestDeepSeekService:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calculate_score_disabled(self): 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) service = DeepSeekService(api_key=None, enabled=False)
with pytest.raises(ScoringError): with pytest.raises(ScoringError):
@@ -281,6 +282,8 @@ class TestScoringManager:
source="rag", source="rag",
model="rubert", model="rubert",
)) ))
mock.add_positive_example = AsyncMock()
mock.add_negative_example = AsyncMock()
return mock return mock
@pytest.fixture @pytest.fixture
@@ -293,6 +296,8 @@ class TestScoringManager:
source="deepseek", source="deepseek",
model="deepseek-chat", model="deepseek-chat",
)) ))
mock.add_positive_example = AsyncMock()
mock.add_negative_example = AsyncMock()
return mock return mock
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -301,7 +306,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager( manager = ScoringManager(
rag_service=mock_rag_service, rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service, deepseek_service=mock_deepseek_service,
) )
@@ -317,7 +322,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager( manager = ScoringManager(
rag_service=mock_rag_service, rag_client=mock_rag_service,
deepseek_service=None, deepseek_service=None,
) )
@@ -331,7 +336,7 @@ class TestScoringManager:
"""Тест скоринга пустого текста.""" """Тест скоринга пустого текста."""
from helper_bot.services.scoring.scoring_manager import ScoringManager 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("") result = await manager.score_post("")
@@ -342,12 +347,12 @@ class TestScoringManager:
async def test_score_post_service_error(self, mock_rag_service, mock_deepseek_service): async def test_score_post_service_error(self, mock_rag_service, mock_deepseek_service):
"""Тест обработки ошибки сервиса.""" """Тест обработки ошибки сервиса."""
from helper_bot.services.scoring.scoring_manager import ScoringManager from helper_bot.services.scoring.scoring_manager import ScoringManager
# RAG выбрасывает ошибку # RAG выбрасывает ошибку
mock_rag_service.calculate_score = AsyncMock(side_effect=Exception("Test error")) mock_rag_service.calculate_score = AsyncMock(side_effect=Exception("Test error"))
manager = ScoringManager( manager = ScoringManager(
rag_service=mock_rag_service, rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service, deepseek_service=mock_deepseek_service,
) )
@@ -365,7 +370,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager( manager = ScoringManager(
rag_service=mock_rag_service, rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service, deepseek_service=mock_deepseek_service,
) )
@@ -380,7 +385,7 @@ class TestScoringManager:
from helper_bot.services.scoring.scoring_manager import ScoringManager from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager( manager = ScoringManager(
rag_service=mock_rag_service, rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service, deepseek_service=mock_deepseek_service,
) )