feat: интеграция ML-скоринга с использованием RAG и DeepSeek

- Обновлен Dockerfile для установки необходимых зависимостей.
- Добавлены новые переменные окружения для настройки ML-скоринга в env.example.
- Реализованы методы для получения и обновления ML-скоров в AsyncBotDB и PostRepository.
- Обновлены обработчики публикации постов для интеграции ML-скоринга.
- Добавлен новый обработчик для получения статистики ML-скоринга в админ-панели.
- Обновлены функции для форматирования сообщений с учетом ML-скоров.
This commit is contained in:
2026-01-26 18:40:38 +03:00
parent e2b1353408
commit 7f6f0f028c
25 changed files with 2833 additions and 52 deletions

View File

@@ -0,0 +1,155 @@
"""
Базовые классы и протоколы для сервисов скоринга.
"""
from dataclasses import dataclass, field
from typing import Optional, Protocol, Dict, Any
from datetime import datetime
@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: Текст отклоненного поста
"""
...