From be8af704ba2286c02dc46f748dd05195c1b72beb Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 26 Jan 2026 22:40:05 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B7=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20PR=20=D0=B2=20Telegram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализована возможность получения тела последнего объединенного PR по коммиту в GitHub Actions. - Добавлен шаг для отправки описания PR в важные логи через Telegram. - Обновлены тесты для проверки нового функционала и улучшения логики обработки сообщений. --- .github/workflows/deploy.yml | 50 +++++++++++++++++++ database/repositories/migration_repository.py | 1 - helper_bot/services/scoring/__init__.py | 15 ++---- helper_bot/services/scoring/base.py | 2 +- .../services/scoring/deepseek_service.py | 5 +- helper_bot/services/scoring/rag_client.py | 8 +-- .../services/scoring/scoring_manager.py | 7 +-- helper_bot/utils/base_dependency_factory.py | 7 +-- tests/test_keyboards_and_filters.py | 9 +++- tests/test_scoring_services.py | 43 +++++++++------- 10 files changed, 100 insertions(+), 47 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d23b853..d26d3cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -172,6 +172,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<> $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 diff --git a/database/repositories/migration_repository.py b/database/repositories/migration_repository.py index 5406416..6dcb67d 100644 --- a/database/repositories/migration_repository.py +++ b/database/repositories/migration_repository.py @@ -1,6 +1,5 @@ """Репозиторий для работы с миграциями базы данных.""" import aiosqlite - from database.base import DatabaseConnection diff --git a/helper_bot/services/scoring/__init__.py b/helper_bot/services/scoring/__init__.py index b0eb339..6a1d156 100644 --- a/helper_bot/services/scoring/__init__.py +++ b/helper_bot/services/scoring/__init__.py @@ -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__ = [ diff --git a/helper_bot/services/scoring/base.py b/helper_bot/services/scoring/base.py index 748afa2..0848468 100644 --- a/helper_bot/services/scoring/base.py +++ b/helper_bot/services/scoring/base.py @@ -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 diff --git a/helper_bot/services/scoring/deepseek_service.py b/helper_bot/services/scoring/deepseek_service.py index de45835..3bd9ecf 100644 --- a/helper_bot/services/scoring/deepseek_service.py +++ b/helper_bot/services/scoring/deepseek_service.py @@ -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 diff --git a/helper_bot/services/scoring/rag_client.py b/helper_bot/services/scoring/rag_client.py index fc35a14..18bb279 100644 --- a/helper_bot/services/scoring/rag_client.py +++ b/helper_bot/services/scoring/rag_client.py @@ -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: diff --git a/helper_bot/services/scoring/scoring_manager.py b/helper_bot/services/scoring/scoring_manager.py index fa23dcb..6c03035 100644 --- a/helper_bot/services/scoring/scoring_manager.py +++ b/helper_bot/services/scoring/scoring_manager.py @@ -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: diff --git a/helper_bot/utils/base_dependency_factory.py b/helper_bot/utils/base_dependency_factory.py index db71fa0..f50c079 100644 --- a/helper_bot/utils/base_dependency_factory.py +++ b/helper_bot/utils/base_dependency_factory.py @@ -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'] diff --git a/tests/test_keyboards_and_filters.py b/tests/test_keyboards_and_filters.py index 539fa8a..f6e25d2 100644 --- a/tests/test_keyboards_and_filters.py +++ b/tests/test_keyboards_and_filters.py @@ -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): """Тест клавиатуры для постов""" diff --git a/tests/test_scoring_services.py b/tests/test_scoring_services.py index 048796b..d827390 100644 --- a/tests/test_scoring_services.py +++ b/tests/test_scoring_services.py @@ -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: @@ -159,7 +157,7 @@ class TestVectorStore: def test_max_examples_limit(self, vector_store): """Тест ограничения максимального количества примеров.""" import numpy as np - + # Добавляем больше чем max_examples for i in range(150): vector = np.random.randn(768).astype(np.float32) @@ -179,7 +177,7 @@ class TestVectorStore: def test_calculate_similarity_with_examples(self, vector_store): """Тест расчета скора с примерами.""" import numpy as np - + # Добавляем положительные примеры for i in range(10): vector = np.random.randn(768).astype(np.float32) @@ -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("") @@ -342,12 +347,12 @@ class TestScoringManager: async def test_score_post_service_error(self, mock_rag_service, mock_deepseek_service): """Тест обработки ошибки сервиса.""" from helper_bot.services.scoring.scoring_manager import ScoringManager - + # RAG выбрасывает ошибку 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, )