""" Базовые классы и протоколы для сервисов скоринга. """ from dataclasses import dataclass, field from datetime import datetime from typing import Any, Dict, Optional, Protocol @dataclass class ScoringResult: """ Результат оценки поста от одного сервиса. Attributes: score: Оценка от 0.0 до 1.0 (вероятность публикации) source: Источник оценки ("deepseek", "rag", etc.) model: Название используемой модели confidence: Уверенность в оценке (опционально) 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}") def to_dict(self) -> Dict[str, Any]: """Преобразует результат в словарь для сохранения в JSON.""" result = { "score": round(self.score, 4), "model": self.model, "ts": self.timestamp, } if self.confidence is not None: result["confidence"] = round(self.confidence, 4) if self.metadata: result["metadata"] = self.metadata return result @classmethod def from_dict(cls, source: str, data: Dict[str, Any]) -> "ScoringResult": """Создает ScoringResult из словаря.""" return cls( score=data["score"], source=source, model=data.get("model", "unknown"), confidence=data.get("confidence"), timestamp=data.get("ts", int(datetime.now().timestamp())), metadata=data.get("metadata", {}), ) @dataclass 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": ...}, "rag": {"score": 0.90, "model": "...", "ts": ...} } """ result = {} if self.deepseek: result["deepseek"] = self.deepseek.to_dict() 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 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: Текст отклоненного поста """ ...