style: isort + black
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user