160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""
|
||
Базовые классы и протоколы для сервисов скоринга.
|
||
"""
|
||
|
||
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: Текст отклоненного поста
|
||
"""
|
||
...
|