style: isort + black
This commit is contained in:
@@ -9,9 +9,14 @@
|
||||
|
||||
from .base import CombinedScore, ScoringResult, ScoringServiceProtocol
|
||||
from .deepseek_service import DeepSeekService
|
||||
from .exceptions import (DeepSeekAPIError, InsufficientExamplesError,
|
||||
ModelNotLoadedError, ScoringError, TextTooShortError,
|
||||
VectorStoreError)
|
||||
from .exceptions import (
|
||||
DeepSeekAPIError,
|
||||
InsufficientExamplesError,
|
||||
ModelNotLoadedError,
|
||||
ScoringError,
|
||||
TextTooShortError,
|
||||
VectorStoreError,
|
||||
)
|
||||
from .rag_client import RagApiClient
|
||||
from .scoring_manager import ScoringManager
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import Any, Dict, Optional, Protocol
|
||||
class ScoringResult:
|
||||
"""
|
||||
Результат оценки поста от одного сервиса.
|
||||
|
||||
|
||||
Attributes:
|
||||
score: Оценка от 0.0 до 1.0 (вероятность публикации)
|
||||
source: Источник оценки ("deepseek", "rag", etc.)
|
||||
@@ -20,18 +20,21 @@ class ScoringResult:
|
||||
timestamp: Время получения оценки
|
||||
metadata: Дополнительные данные
|
||||
"""
|
||||
|
||||
score: float
|
||||
source: str
|
||||
model: str
|
||||
confidence: Optional[float] = None
|
||||
timestamp: int = field(default_factory=lambda: int(datetime.now().timestamp()))
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
"""Валидация score в диапазоне [0.0, 1.0]."""
|
||||
if not 0.0 <= self.score <= 1.0:
|
||||
raise ValueError(f"Score должен быть в диапазоне [0.0, 1.0], получено: {self.score}")
|
||||
|
||||
raise ValueError(
|
||||
f"Score должен быть в диапазоне [0.0, 1.0], получено: {self.score}"
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Преобразует результат в словарь для сохранения в JSON."""
|
||||
result = {
|
||||
@@ -44,7 +47,7 @@ class ScoringResult:
|
||||
if self.metadata:
|
||||
result["metadata"] = self.metadata
|
||||
return result
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, source: str, data: Dict[str, Any]) -> "ScoringResult":
|
||||
"""Создает ScoringResult из словаря."""
|
||||
@@ -62,30 +65,31 @@ class ScoringResult:
|
||||
class CombinedScore:
|
||||
"""
|
||||
Объединенный результат от всех сервисов скоринга.
|
||||
|
||||
|
||||
Attributes:
|
||||
deepseek: Результат от DeepSeek API (None если отключен/ошибка)
|
||||
rag: Результат от RAG сервиса (None если отключен/ошибка)
|
||||
errors: Словарь с ошибками по источникам
|
||||
"""
|
||||
|
||||
deepseek: Optional[ScoringResult] = None
|
||||
rag: Optional[ScoringResult] = None
|
||||
errors: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@property
|
||||
def deepseek_score(self) -> Optional[float]:
|
||||
"""Возвращает только числовой скор от DeepSeek."""
|
||||
return self.deepseek.score if self.deepseek else None
|
||||
|
||||
|
||||
@property
|
||||
def rag_score(self) -> Optional[float]:
|
||||
"""Возвращает только числовой скор от RAG."""
|
||||
return self.rag.score if self.rag else None
|
||||
|
||||
|
||||
def to_json_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Преобразует в словарь для сохранения в ml_scores колонку.
|
||||
|
||||
|
||||
Формат:
|
||||
{
|
||||
"deepseek": {"score": 0.75, "model": "...", "ts": ...},
|
||||
@@ -98,7 +102,7 @@ class CombinedScore:
|
||||
if self.rag:
|
||||
result["rag"] = self.rag.to_dict()
|
||||
return result
|
||||
|
||||
|
||||
def has_any_score(self) -> bool:
|
||||
"""Проверяет, есть ли хотя бы один успешный скор."""
|
||||
return self.deepseek is not None or self.rag is not None
|
||||
@@ -107,48 +111,48 @@ class CombinedScore:
|
||||
class ScoringServiceProtocol(Protocol):
|
||||
"""
|
||||
Протокол для сервисов скоринга.
|
||||
|
||||
|
||||
Любой сервис скоринга должен реализовывать эти методы.
|
||||
"""
|
||||
|
||||
|
||||
@property
|
||||
def source_name(self) -> str:
|
||||
"""Возвращает имя источника ("deepseek", "rag", etc.)."""
|
||||
...
|
||||
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
"""Проверяет, включен ли сервис."""
|
||||
...
|
||||
|
||||
|
||||
async def calculate_score(self, text: str) -> ScoringResult:
|
||||
"""
|
||||
Рассчитывает скор для текста поста.
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст поста для оценки
|
||||
|
||||
|
||||
Returns:
|
||||
ScoringResult с оценкой
|
||||
|
||||
|
||||
Raises:
|
||||
ScoringError: При ошибке расчета
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
async def add_positive_example(self, text: str) -> None:
|
||||
"""
|
||||
Добавляет текст как положительный пример (опубликованный пост).
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст опубликованного поста
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
async def add_negative_example(self, text: str) -> None:
|
||||
"""
|
||||
Добавляет текст как отрицательный пример (отклоненный пост).
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст отклоненного поста
|
||||
"""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,29 +5,35 @@
|
||||
|
||||
class ScoringError(Exception):
|
||||
"""Базовое исключение для ошибок скоринга."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ModelNotLoadedError(ScoringError):
|
||||
"""Модель не загружена или недоступна."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VectorStoreError(ScoringError):
|
||||
"""Ошибка при работе с хранилищем векторов."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DeepSeekAPIError(ScoringError):
|
||||
"""Ошибка при обращении к DeepSeek API."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientExamplesError(ScoringError):
|
||||
"""Недостаточно примеров для расчета скора."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TextTooShortError(ScoringError):
|
||||
"""Текст слишком короткий для векторизации."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -7,24 +7,24 @@ HTTP клиент для взаимодействия с внешним RAG се
|
||||
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 .base import ScoringResult
|
||||
from .exceptions import (InsufficientExamplesError, ScoringError,
|
||||
TextTooShortError)
|
||||
from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError
|
||||
|
||||
|
||||
class RagApiClient:
|
||||
"""
|
||||
HTTP клиент для взаимодействия с внешним RAG сервисом.
|
||||
|
||||
|
||||
Использует REST API для:
|
||||
- Получения скоров постов (POST /api/v1/score)
|
||||
- Отправки положительных примеров (POST /api/v1/examples/positive)
|
||||
- Отправки отрицательных примеров (POST /api/v1/examples/negative)
|
||||
- Получения статистики (GET /api/v1/stats)
|
||||
|
||||
|
||||
Attributes:
|
||||
api_url: Базовый URL API сервиса
|
||||
api_key: API ключ для аутентификации
|
||||
@@ -32,7 +32,7 @@ class RagApiClient:
|
||||
test_mode: Включен ли тестовый режим (добавляет заголовок X-Test-Mode: true)
|
||||
enabled: Включен ли клиент
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_url: str,
|
||||
@@ -43,7 +43,7 @@ class RagApiClient:
|
||||
):
|
||||
"""
|
||||
Инициализация клиента.
|
||||
|
||||
|
||||
Args:
|
||||
api_url: Базовый URL API (например, http://хх.ххх.ххх.хх/api/v1)
|
||||
api_key: API ключ для аутентификации
|
||||
@@ -52,49 +52,51 @@ class RagApiClient:
|
||||
enabled: Включен ли клиент
|
||||
"""
|
||||
# Убираем trailing slash если есть
|
||||
self.api_url = api_url.rstrip('/')
|
||||
self.api_url = api_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
self.test_mode = test_mode
|
||||
self._enabled = enabled
|
||||
|
||||
|
||||
# Создаем HTTP клиент
|
||||
self._client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(timeout),
|
||||
headers={
|
||||
"X-API-Key": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(f"RagApiClient инициализирован (url={self.api_url}, enabled={enabled})")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"RagApiClient инициализирован (url={self.api_url}, enabled={enabled})"
|
||||
)
|
||||
|
||||
@property
|
||||
def source_name(self) -> str:
|
||||
"""Имя источника для результатов."""
|
||||
return "rag"
|
||||
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
"""Проверяет, включен ли клиент."""
|
||||
return self._enabled
|
||||
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Закрывает HTTP клиент."""
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
@track_time("calculate_score", "rag_client")
|
||||
@track_errors("rag_client", "calculate_score")
|
||||
async def calculate_score(self, text: str) -> ScoringResult:
|
||||
"""
|
||||
Рассчитывает скор для текста поста через API.
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст поста для оценки
|
||||
|
||||
|
||||
Returns:
|
||||
ScoringResult с оценкой
|
||||
|
||||
|
||||
Raises:
|
||||
ScoringError: При ошибке расчета
|
||||
InsufficientExamplesError: Если недостаточно примеров
|
||||
@@ -102,16 +104,15 @@ class RagApiClient:
|
||||
"""
|
||||
if not self._enabled:
|
||||
raise ScoringError("RAG API клиент отключен")
|
||||
|
||||
|
||||
if not text or not text.strip():
|
||||
raise TextTooShortError("Текст пустой")
|
||||
|
||||
|
||||
try:
|
||||
response = await self._client.post(
|
||||
f"{self.api_url}/score",
|
||||
json={"text": text.strip()}
|
||||
f"{self.api_url}/score", json={"text": text.strip()}
|
||||
)
|
||||
|
||||
|
||||
# Обрабатываем различные статусы
|
||||
if response.status_code == 400:
|
||||
try:
|
||||
@@ -119,57 +120,70 @@ class RagApiClient:
|
||||
error_msg = error_data.get("detail", "Неизвестная ошибка")
|
||||
except Exception:
|
||||
error_msg = response.text or "Неизвестная ошибка"
|
||||
|
||||
|
||||
logger.warning(f"RagApiClient: Ошибка валидации запроса: {error_msg}")
|
||||
|
||||
if "недостаточно" in error_msg.lower() or "insufficient" in error_msg.lower():
|
||||
|
||||
if (
|
||||
"недостаточно" in error_msg.lower()
|
||||
or "insufficient" in error_msg.lower()
|
||||
):
|
||||
raise InsufficientExamplesError(error_msg)
|
||||
if "коротк" in error_msg.lower() or "short" in error_msg.lower():
|
||||
raise TextTooShortError(error_msg)
|
||||
raise ScoringError(f"Ошибка валидации: {error_msg}")
|
||||
|
||||
|
||||
if response.status_code == 401:
|
||||
logger.error("RagApiClient: Ошибка аутентификации: неверный API ключ")
|
||||
raise ScoringError("Ошибка аутентификации: неверный API ключ")
|
||||
|
||||
|
||||
if response.status_code == 404:
|
||||
logger.error("RagApiClient: RAG API endpoint не найден")
|
||||
raise ScoringError("RAG API endpoint не найден")
|
||||
|
||||
|
||||
if response.status_code >= 500:
|
||||
logger.error(f"RagApiClient: Ошибка сервера RAG API: {response.status_code}")
|
||||
logger.error(
|
||||
f"RagApiClient: Ошибка сервера RAG API: {response.status_code}"
|
||||
)
|
||||
raise ScoringError(f"Ошибка сервера RAG API: {response.status_code}")
|
||||
|
||||
|
||||
# Проверяем успешный статус
|
||||
if response.status_code != 200:
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
data = response.json()
|
||||
|
||||
|
||||
# Парсим ответ
|
||||
score = float(data.get("rag_score", 0.0))
|
||||
confidence = float(data.get("rag_confidence", 0.0)) if data.get("rag_confidence") is not None else None
|
||||
|
||||
confidence = (
|
||||
float(data.get("rag_confidence", 0.0))
|
||||
if data.get("rag_confidence") is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# Форматируем confidence для логирования
|
||||
confidence_str = f"{confidence:.4f}" if confidence is not None else "None"
|
||||
|
||||
|
||||
logger.info(
|
||||
f"RagApiClient: Скор успешно получен "
|
||||
f"(score={score:.4f}, confidence={confidence_str})"
|
||||
)
|
||||
|
||||
|
||||
return ScoringResult(
|
||||
score=score,
|
||||
source=self.source_name,
|
||||
model=data.get("meta", {}).get("model", "rag-service"),
|
||||
confidence=confidence,
|
||||
metadata={
|
||||
"rag_score_pos_only": float(data.get("rag_score_pos_only", 0.0)) if data.get("rag_score_pos_only") is not None else None,
|
||||
"rag_score_pos_only": (
|
||||
float(data.get("rag_score_pos_only", 0.0))
|
||||
if data.get("rag_score_pos_only") is not None
|
||||
else None
|
||||
),
|
||||
"positive_examples": data.get("meta", {}).get("positive_examples"),
|
||||
"negative_examples": data.get("meta", {}).get("negative_examples"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error(f"RagApiClient: Таймаут запроса к RAG API (>{self.timeout}с)")
|
||||
raise ScoringError(f"Таймаут запроса к RAG API (>{self.timeout}с)")
|
||||
@@ -177,7 +191,9 @@ class RagApiClient:
|
||||
logger.error(f"RagApiClient: Ошибка подключения к RAG API: {e}")
|
||||
raise ScoringError(f"Ошибка подключения к RAG API: {e}")
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
logger.error(f"RagApiClient: Ошибка парсинга ответа: {e}, response: {response.text if 'response' in locals() else 'N/A'}")
|
||||
logger.error(
|
||||
f"RagApiClient: Ошибка парсинга ответа: {e}, response: {response.text if 'response' in locals() else 'N/A'}"
|
||||
)
|
||||
raise ScoringError(f"Ошибка парсинга ответа от RAG API: {e}")
|
||||
except InsufficientExamplesError:
|
||||
raise
|
||||
@@ -188,122 +204,145 @@ class RagApiClient:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Только действительно неожиданные ошибки логируем здесь
|
||||
logger.error(f"RagApiClient: Неожиданная ошибка при расчете скора: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"RagApiClient: Неожиданная ошибка при расчете скора: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise ScoringError(f"Неожиданная ошибка: {e}")
|
||||
|
||||
|
||||
@track_time("add_positive_example", "rag_client")
|
||||
async def add_positive_example(self, text: str) -> None:
|
||||
"""
|
||||
Добавляет текст как положительный пример (опубликованный пост).
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст опубликованного поста
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
|
||||
if not text or not text.strip():
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# Формируем заголовки (добавляем X-Test-Mode если включен тестовый режим)
|
||||
headers = {}
|
||||
if self.test_mode:
|
||||
headers["X-Test-Mode"] = "true"
|
||||
|
||||
|
||||
response = await self._client.post(
|
||||
f"{self.api_url}/examples/positive",
|
||||
json={"text": text.strip()},
|
||||
headers=headers
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
if response.status_code == 200 or response.status_code == 201:
|
||||
logger.info("RagApiClient: Положительный пример успешно добавлен")
|
||||
elif response.status_code == 400:
|
||||
logger.warning(f"RagApiClient: Ошибка валидации при добавлении положительного примера: {response.text}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка валидации при добавлении положительного примера: {response.text}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"RagApiClient: Неожиданный статус при добавлении положительного примера: {response.status_code}")
|
||||
|
||||
logger.warning(
|
||||
f"RagApiClient: Неожиданный статус при добавлении положительного примера: {response.status_code}"
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"RagApiClient: Таймаут при добавлении положительного примера")
|
||||
logger.warning(
|
||||
f"RagApiClient: Таймаут при добавлении положительного примера"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.warning(f"RagApiClient: Ошибка подключения при добавлении положительного примера: {e}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка подключения при добавлении положительного примера: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"RagApiClient: Ошибка добавления положительного примера: {e}")
|
||||
|
||||
|
||||
@track_time("add_negative_example", "rag_client")
|
||||
async def add_negative_example(self, text: str) -> None:
|
||||
"""
|
||||
Добавляет текст как отрицательный пример (отклоненный пост).
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст отклоненного поста
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
|
||||
if not text or not text.strip():
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# Формируем заголовки (добавляем X-Test-Mode если включен тестовый режим)
|
||||
headers = {}
|
||||
if self.test_mode:
|
||||
headers["X-Test-Mode"] = "true"
|
||||
|
||||
|
||||
response = await self._client.post(
|
||||
f"{self.api_url}/examples/negative",
|
||||
json={"text": text.strip()},
|
||||
headers=headers
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
if response.status_code == 200 or response.status_code == 201:
|
||||
logger.info("RagApiClient: Отрицательный пример успешно добавлен")
|
||||
elif response.status_code == 400:
|
||||
logger.warning(f"RagApiClient: Ошибка валидации при добавлении отрицательного примера: {response.text}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка валидации при добавлении отрицательного примера: {response.text}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"RagApiClient: Неожиданный статус при добавлении отрицательного примера: {response.status_code}")
|
||||
|
||||
logger.warning(
|
||||
f"RagApiClient: Неожиданный статус при добавлении отрицательного примера: {response.status_code}"
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"RagApiClient: Таймаут при добавлении отрицательного примера")
|
||||
logger.warning(
|
||||
f"RagApiClient: Таймаут при добавлении отрицательного примера"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.warning(f"RagApiClient: Ошибка подключения при добавлении отрицательного примера: {e}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка подключения при добавлении отрицательного примера: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"RagApiClient: Ошибка добавления отрицательного примера: {e}")
|
||||
|
||||
|
||||
async def get_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Получает статистику от RAG API через endpoint /stats.
|
||||
|
||||
|
||||
Returns:
|
||||
Словарь со статистикой или пустой словарь при ошибке
|
||||
"""
|
||||
if not self._enabled:
|
||||
return {}
|
||||
|
||||
|
||||
try:
|
||||
response = await self._client.get(f"{self.api_url}/stats")
|
||||
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.warning(f"RagApiClient: Неожиданный статус при получении статистики: {response.status_code}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Неожиданный статус при получении статистики: {response.status_code}"
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"RagApiClient: Таймаут при получении статистики")
|
||||
return {}
|
||||
except httpx.RequestError as e:
|
||||
logger.warning(f"RagApiClient: Ошибка подключения при получении статистики: {e}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка подключения при получении статистики: {e}"
|
||||
)
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"RagApiClient: Ошибка получения статистики: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def get_stats_sync(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Синхронная версия get_stats для использования в get_stats() ScoringManager.
|
||||
|
||||
|
||||
Внимание: Это заглушка, реальная статистика будет получена асинхронно.
|
||||
"""
|
||||
return {
|
||||
|
||||
@@ -13,23 +13,22 @@ from logs.custom_logger import logger
|
||||
|
||||
from .base import CombinedScore, ScoringResult
|
||||
from .deepseek_service import DeepSeekService
|
||||
from .exceptions import (InsufficientExamplesError, ScoringError,
|
||||
TextTooShortError)
|
||||
from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError
|
||||
from .rag_client import RagApiClient
|
||||
|
||||
|
||||
class ScoringManager:
|
||||
"""
|
||||
Менеджер для управления всеми сервисами скоринга.
|
||||
|
||||
|
||||
Объединяет RagApiClient и DeepSeekService, выполняет параллельные
|
||||
запросы и агрегирует результаты в единый CombinedScore.
|
||||
|
||||
|
||||
Attributes:
|
||||
rag_client: HTTP клиент для RAG API
|
||||
deepseek_service: Сервис DeepSeek API
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rag_client: Optional[RagApiClient] = None,
|
||||
@@ -37,68 +36,70 @@ class ScoringManager:
|
||||
):
|
||||
"""
|
||||
Инициализация менеджера.
|
||||
|
||||
|
||||
Args:
|
||||
rag_client: HTTP клиент для RAG API (создается автоматически если не передан)
|
||||
deepseek_service: Сервис DeepSeek (создается автоматически если не передан)
|
||||
"""
|
||||
self.rag_client = rag_client
|
||||
self.deepseek_service = deepseek_service
|
||||
|
||||
|
||||
logger.info(
|
||||
f"ScoringManager инициализирован "
|
||||
f"(rag={rag_client is not None and rag_client.is_enabled}, "
|
||||
f"deepseek={deepseek_service is not None and deepseek_service.is_enabled})"
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def is_any_enabled(self) -> bool:
|
||||
"""Проверяет, включен ли хотя бы один сервис."""
|
||||
rag_enabled = self.rag_client is not None and self.rag_client.is_enabled
|
||||
deepseek_enabled = self.deepseek_service is not None and self.deepseek_service.is_enabled
|
||||
deepseek_enabled = (
|
||||
self.deepseek_service is not None and self.deepseek_service.is_enabled
|
||||
)
|
||||
return rag_enabled or deepseek_enabled
|
||||
|
||||
|
||||
@track_time("score_post", "scoring_manager")
|
||||
@track_errors("scoring_manager", "score_post")
|
||||
async def score_post(self, text: str) -> CombinedScore:
|
||||
"""
|
||||
Рассчитывает скоры для текста поста от всех сервисов.
|
||||
|
||||
|
||||
Выполняет запросы параллельно для минимизации задержки.
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст поста для оценки
|
||||
|
||||
|
||||
Returns:
|
||||
CombinedScore с результатами от всех сервисов
|
||||
"""
|
||||
result = CombinedScore()
|
||||
|
||||
|
||||
if not text or not text.strip():
|
||||
logger.debug("ScoringManager: Пустой текст, пропускаем скоринг")
|
||||
return result
|
||||
|
||||
|
||||
# Собираем задачи для параллельного выполнения
|
||||
tasks = []
|
||||
task_names = []
|
||||
|
||||
|
||||
# RAG API клиент
|
||||
if self.rag_client and self.rag_client.is_enabled:
|
||||
tasks.append(self._get_rag_score(text))
|
||||
task_names.append("rag")
|
||||
|
||||
|
||||
# DeepSeek сервис
|
||||
if self.deepseek_service and self.deepseek_service.is_enabled:
|
||||
tasks.append(self._get_deepseek_score(text))
|
||||
task_names.append("deepseek")
|
||||
|
||||
|
||||
if not tasks:
|
||||
logger.debug("ScoringManager: Нет активных сервисов для скоринга")
|
||||
return result
|
||||
|
||||
|
||||
# Выполняем параллельно
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
# Обрабатываем результаты
|
||||
for name, res in zip(task_names, results):
|
||||
if isinstance(res, Exception):
|
||||
@@ -111,14 +112,14 @@ class ScoringManager:
|
||||
result.rag = res
|
||||
elif name == "deepseek":
|
||||
result.deepseek = res
|
||||
|
||||
|
||||
logger.info(
|
||||
f"ScoringManager: Скоринг завершен "
|
||||
f"(rag={result.rag_score}, deepseek={result.deepseek_score})"
|
||||
)
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _get_rag_score(self, text: str) -> Optional[ScoringResult]:
|
||||
"""Получает скор от RAG API."""
|
||||
try:
|
||||
@@ -134,7 +135,7 @@ class ScoringManager:
|
||||
except Exception as e:
|
||||
# Ошибки уже залогированы в RagApiClient, здесь только пробрасываем
|
||||
raise
|
||||
|
||||
|
||||
async def _get_deepseek_score(self, text: str) -> Optional[ScoringResult]:
|
||||
"""Получает скор от DeepSeek сервиса."""
|
||||
try:
|
||||
@@ -146,78 +147,77 @@ class ScoringManager:
|
||||
except Exception as e:
|
||||
# Ошибки уже залогированы в DeepSeekService, здесь только пробрасываем
|
||||
raise
|
||||
|
||||
|
||||
@track_time("on_post_published", "scoring_manager")
|
||||
async def on_post_published(self, text: str) -> None:
|
||||
"""
|
||||
Вызывается при публикации поста.
|
||||
|
||||
|
||||
Добавляет текст как положительный пример для обучения RAG.
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст опубликованного поста
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return
|
||||
|
||||
|
||||
tasks = []
|
||||
|
||||
|
||||
if self.rag_client and self.rag_client.is_enabled:
|
||||
tasks.append(self.rag_client.add_positive_example(text))
|
||||
|
||||
|
||||
if self.deepseek_service and self.deepseek_service.is_enabled:
|
||||
tasks.append(self.deepseek_service.add_positive_example(text))
|
||||
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info("ScoringManager: Добавлен положительный пример")
|
||||
|
||||
|
||||
@track_time("on_post_declined", "scoring_manager")
|
||||
async def on_post_declined(self, text: str) -> None:
|
||||
"""
|
||||
Вызывается при отклонении поста.
|
||||
|
||||
|
||||
Добавляет текст как отрицательный пример для обучения RAG.
|
||||
|
||||
|
||||
Args:
|
||||
text: Текст отклоненного поста
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return
|
||||
|
||||
|
||||
tasks = []
|
||||
|
||||
|
||||
if self.rag_client and self.rag_client.is_enabled:
|
||||
tasks.append(self.rag_client.add_negative_example(text))
|
||||
|
||||
|
||||
if self.deepseek_service and self.deepseek_service.is_enabled:
|
||||
tasks.append(self.deepseek_service.add_negative_example(text))
|
||||
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info("ScoringManager: Добавлен отрицательный пример")
|
||||
|
||||
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Закрывает ресурсы всех сервисов."""
|
||||
if self.deepseek_service:
|
||||
await self.deepseek_service.close()
|
||||
|
||||
|
||||
if self.rag_client:
|
||||
await self.rag_client.close()
|
||||
|
||||
|
||||
async def get_stats(self) -> dict:
|
||||
"""Возвращает статистику всех сервисов."""
|
||||
stats = {
|
||||
"any_enabled": self.is_any_enabled,
|
||||
}
|
||||
|
||||
|
||||
if self.rag_client:
|
||||
# Получаем статистику асинхронно от API
|
||||
rag_stats = await self.rag_client.get_stats()
|
||||
stats["rag"] = rag_stats if rag_stats else self.rag_client.get_stats_sync()
|
||||
|
||||
|
||||
if self.deepseek_service:
|
||||
stats["deepseek"] = self.deepseek_service.get_stats()
|
||||
|
||||
|
||||
return stats
|
||||
|
||||
Reference in New Issue
Block a user