style: isort + black

This commit is contained in:
2026-02-02 00:13:33 +03:00
parent 5f66c86d99
commit 561c9074dd
86 changed files with 8459 additions and 5793 deletions

View File

@@ -9,6 +9,7 @@ import json
from typing import List, Optional
import httpx
from helper_bot.utils.metrics import track_errors, track_time
from logs.custom_logger import logger
@@ -19,17 +20,17 @@ from .exceptions import DeepSeekAPIError, ScoringError, TextTooShortError
class DeepSeekService:
"""
Сервис для оценки постов через DeepSeek API.
Отправляет текст поста в DeepSeek с промптом для оценки
и получает числовой скор релевантности.
Attributes:
api_key: API ключ DeepSeek
api_url: URL API эндпоинта
model: Название модели
timeout: Таймаут запроса в секундах
"""
# Промпт для оценки поста
SCORING_PROMPT = """Роль: Ты — строгий и внимательный модератор сообщества в социальной сети, ориентированного на знакомства между людьми. Твоя задача — оценить, можно ли опубликовать пост, основываясь на четких правилах.
@@ -77,7 +78,7 @@ class DeepSeekService:
DEFAULT_API_URL = "https://api.deepseek.com/v1/chat/completions"
DEFAULT_MODEL = "deepseek-chat"
def __init__(
self,
api_key: Optional[str] = None,
@@ -90,7 +91,7 @@ class DeepSeekService:
):
"""
Инициализация DeepSeek сервиса.
Args:
api_key: API ключ DeepSeek
api_url: URL API эндпоинта
@@ -107,29 +108,29 @@ class DeepSeekService:
self._enabled = enabled and bool(api_key)
self.min_text_length = min_text_length
self.max_retries = max_retries
# HTTP клиент (создается лениво)
self._client: Optional[httpx.AsyncClient] = None
if not api_key and enabled:
logger.warning("DeepSeekService: API ключ не указан, сервис отключен")
self._enabled = False
logger.info(
f"DeepSeekService инициализирован "
f"(model={self.model}, enabled={self._enabled})"
)
@property
def source_name(self) -> str:
"""Имя источника для результатов."""
return "deepseek"
@property
def is_enabled(self) -> bool:
"""Проверяет, включен ли сервис."""
return self._enabled
async def _get_client(self) -> httpx.AsyncClient:
"""Получает или создает HTTP клиент."""
if self._client is None:
@@ -141,101 +142,106 @@ class DeepSeekService:
},
)
return self._client
async def close(self) -> None:
"""Закрывает HTTP клиент."""
if self._client:
await self._client.aclose()
self._client = None
def _clean_text(self, text: str) -> str:
"""Очищает текст от лишних символов."""
if not text:
return ""
# Удаляем лишние пробелы и переносы строк
clean = " ".join(text.split())
# Удаляем служебные символы
if clean == "^":
return ""
return clean.strip()
def _parse_score_response(self, response_text: str) -> float:
"""
Парсит ответ от DeepSeek и извлекает скор.
Args:
response_text: Текст ответа от API
Returns:
Числовой скор от 0.0 до 1.0
Raises:
DeepSeekAPIError: Если не удалось распарсить ответ
"""
try:
# Пытаемся найти число в ответе
text = response_text.strip()
# Убираем возможные обрамления
text = text.strip('"\'`')
text = text.strip("\"'`")
# Пробуем распарсить как число
score = float(text)
# Ограничиваем диапазон
score = max(0.0, min(1.0, score))
return score
except ValueError:
# Пробуем найти число в тексте
import re
matches = re.findall(r'0\.\d+|1\.0|0|1', text)
matches = re.findall(r"0\.\d+|1\.0|0|1", text)
if matches:
score = float(matches[0])
return max(0.0, min(1.0, score))
logger.error(f"DeepSeekService: Не удалось распарсить ответ: {response_text}")
raise DeepSeekAPIError(f"Не удалось распарсить скор из ответа: {response_text}")
logger.error(
f"DeepSeekService: Не удалось распарсить ответ: {response_text}"
)
raise DeepSeekAPIError(
f"Не удалось распарсить скор из ответа: {response_text}"
)
@track_time("calculate_score", "deepseek_service")
@track_errors("deepseek_service", "calculate_score")
async def calculate_score(self, text: str) -> ScoringResult:
"""
Рассчитывает скор для текста поста через DeepSeek API.
Args:
text: Текст поста для оценки
Returns:
ScoringResult с оценкой
Raises:
ScoringError: При ошибке расчета
"""
if not self._enabled:
raise ScoringError("DeepSeek сервис отключен")
# Очищаем текст
clean_text = self._clean_text(text)
if len(clean_text) < self.min_text_length:
raise TextTooShortError(
f"Текст слишком короткий (минимум {self.min_text_length} символов)"
)
# Формируем промпт
prompt = self.SCORING_PROMPT.format(text=clean_text)
# Выполняем запрос с повторными попытками
last_error = None
for attempt in range(self.max_retries):
try:
score = await self._make_api_request(prompt)
return ScoringResult(
score=score,
source=self.source_name,
@@ -245,7 +251,7 @@ class DeepSeekService:
"attempt": attempt + 1,
},
)
except DeepSeekAPIError as e:
last_error = e
logger.warning(
@@ -254,25 +260,27 @@ class DeepSeekService:
)
if attempt < self.max_retries - 1:
# Экспоненциальная задержка
await asyncio.sleep(2 ** attempt)
raise ScoringError(f"Все попытки запроса к DeepSeek API не удались: {last_error}")
await asyncio.sleep(2**attempt)
raise ScoringError(
f"Все попытки запроса к DeepSeek API не удались: {last_error}"
)
async def _make_api_request(self, prompt: str) -> float:
"""
Выполняет запрос к DeepSeek API.
Args:
prompt: Промпт для отправки
Returns:
Числовой скор от 0.0 до 1.0
Raises:
DeepSeekAPIError: При ошибке API
"""
client = await self._get_client()
payload = {
"model": self.model,
"messages": [
@@ -282,27 +290,27 @@ class DeepSeekService:
}
],
"temperature": 0.1, # Низкая температура для детерминированности
"max_tokens": 10, # Ожидаем только число
"max_tokens": 10, # Ожидаем только число
}
try:
response = await client.post(self.api_url, json=payload)
response.raise_for_status()
data = response.json()
# Извлекаем ответ
if "choices" not in data or not data["choices"]:
raise DeepSeekAPIError("Пустой ответ от API")
response_text = data["choices"][0]["message"]["content"]
# Парсим скор
score = self._parse_score_response(response_text)
logger.debug(f"DeepSeekService: Получен скор {score} для текста")
return score
except httpx.HTTPStatusError as e:
error_msg = f"HTTP ошибка {e.response.status_code}"
try:
@@ -312,40 +320,40 @@ class DeepSeekService:
except Exception:
pass
raise DeepSeekAPIError(error_msg)
except httpx.TimeoutException:
raise DeepSeekAPIError(f"Таймаут запроса ({self.timeout}s)")
except Exception as e:
raise DeepSeekAPIError(f"Ошибка запроса: {e}")
async def add_positive_example(self, text: str) -> None:
"""
Добавляет текст как положительный пример.
Для DeepSeek не требуется хранить примеры - оценка выполняется
на основе промпта. Метод существует для совместимости с протоколом.
Args:
text: Текст опубликованного поста
"""
# DeepSeek не использует примеры для обучения
# Промпт уже содержит критерии оценки
pass
async def add_negative_example(self, text: str) -> None:
"""
Добавляет текст как отрицательный пример.
Для DeepSeek не требуется хранить примеры - оценка выполняется
на основе промпта. Метод существует для совместимости с протоколом.
Args:
text: Текст отклоненного поста
"""
# DeepSeek не использует примеры для обучения
pass
def get_stats(self) -> dict:
"""Возвращает статистику сервиса."""
return {