feat: интеграция ML-скоринга с использованием RAG и DeepSeek
- Обновлен Dockerfile для установки необходимых зависимостей. - Добавлены новые переменные окружения для настройки ML-скоринга в env.example. - Реализованы методы для получения и обновления ML-скоров в AsyncBotDB и PostRepository. - Обновлены обработчики публикации постов для интеграции ML-скоринга. - Добавлен новый обработчик для получения статистики ML-скоринга в админ-панели. - Обновлены функции для форматирования сообщений с учетом ML-скоров.
This commit is contained in:
@@ -5,6 +5,7 @@ from typing import Optional
|
||||
from database.async_db import AsyncBotDB
|
||||
from dotenv import load_dotenv
|
||||
from helper_bot.utils.s3_storage import S3StorageService
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class BaseDependencyFactory:
|
||||
@@ -15,6 +16,7 @@ class BaseDependencyFactory:
|
||||
load_dotenv(env_path)
|
||||
|
||||
self.settings = {}
|
||||
self._project_dir = project_dir
|
||||
|
||||
database_path = os.getenv('DATABASE_PATH', 'database/tg-bot-database.db')
|
||||
if not os.path.isabs(database_path):
|
||||
@@ -24,6 +26,9 @@ class BaseDependencyFactory:
|
||||
|
||||
self._load_settings_from_env()
|
||||
self._init_s3_storage()
|
||||
|
||||
# ScoringManager инициализируется лениво
|
||||
self._scoring_manager = None
|
||||
|
||||
def _load_settings_from_env(self):
|
||||
"""Загружает настройки из переменных окружения."""
|
||||
@@ -59,6 +64,23 @@ class BaseDependencyFactory:
|
||||
'bucket_name': os.getenv('S3_BUCKET_NAME', ''),
|
||||
'region': os.getenv('S3_REGION', 'us-east-1')
|
||||
}
|
||||
|
||||
# Настройки ML-скоринга
|
||||
self.settings['Scoring'] = {
|
||||
# RAG (ruBERT)
|
||||
'rag_enabled': self._parse_bool(os.getenv('RAG_ENABLED', 'false')),
|
||||
'rag_model': os.getenv('RAG_MODEL', 'DeepPavlov/rubert-base-cased'),
|
||||
'rag_cache_dir': os.getenv('RAG_CACHE_DIR', 'data/models'),
|
||||
'rag_vectors_path': os.getenv('RAG_VECTORS_PATH', 'data/vectors.npz'),
|
||||
'rag_max_examples': self._parse_int(os.getenv('RAG_MAX_EXAMPLES', '10000')),
|
||||
'rag_score_multiplier': self._parse_float(os.getenv('RAG_SCORE_MULTIPLIER', '5.0')),
|
||||
# DeepSeek
|
||||
'deepseek_enabled': self._parse_bool(os.getenv('DEEPSEEK_ENABLED', 'false')),
|
||||
'deepseek_api_key': os.getenv('DEEPSEEK_API_KEY', ''),
|
||||
'deepseek_api_url': os.getenv('DEEPSEEK_API_URL', 'https://api.deepseek.com/v1/chat/completions'),
|
||||
'deepseek_model': os.getenv('DEEPSEEK_MODEL', 'deepseek-chat'),
|
||||
'deepseek_timeout': self._parse_int(os.getenv('DEEPSEEK_TIMEOUT', '30')),
|
||||
}
|
||||
|
||||
def _init_s3_storage(self):
|
||||
"""Инициализирует S3StorageService если S3 включен."""
|
||||
@@ -84,6 +106,13 @@ class BaseDependencyFactory:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
def _parse_float(self, value: str) -> float:
|
||||
"""Парсит строковое значение в float."""
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
def get_settings(self):
|
||||
return self.settings
|
||||
@@ -95,6 +124,100 @@ class BaseDependencyFactory:
|
||||
def get_s3_storage(self) -> Optional[S3StorageService]:
|
||||
"""Возвращает S3StorageService если S3 включен, иначе None."""
|
||||
return self.s3_storage
|
||||
|
||||
def _init_scoring_manager(self):
|
||||
"""
|
||||
Инициализирует ScoringManager с RAG и DeepSeek сервисами.
|
||||
|
||||
Вызывается лениво при первом обращении к get_scoring_manager().
|
||||
"""
|
||||
from helper_bot.services.scoring import (
|
||||
ScoringManager,
|
||||
RAGService,
|
||||
DeepSeekService,
|
||||
VectorStore,
|
||||
)
|
||||
|
||||
scoring_config = self.settings['Scoring']
|
||||
|
||||
# Инициализация RAG сервиса
|
||||
rag_service = None
|
||||
if scoring_config['rag_enabled']:
|
||||
# Путь к векторам
|
||||
vectors_path = scoring_config['rag_vectors_path']
|
||||
if not os.path.isabs(vectors_path):
|
||||
vectors_path = os.path.join(self._project_dir, vectors_path)
|
||||
|
||||
# Путь к кешу моделей
|
||||
cache_dir = scoring_config['rag_cache_dir']
|
||||
if not os.path.isabs(cache_dir):
|
||||
cache_dir = os.path.join(self._project_dir, cache_dir)
|
||||
|
||||
# Создаем директории если нужно
|
||||
os.makedirs(os.path.dirname(vectors_path), exist_ok=True)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
# Создаем VectorStore
|
||||
vector_store = VectorStore(
|
||||
vector_dim=768, # ruBERT dimension
|
||||
max_examples=scoring_config['rag_max_examples'],
|
||||
storage_path=vectors_path,
|
||||
score_multiplier=scoring_config['rag_score_multiplier'],
|
||||
)
|
||||
|
||||
# Создаем RAGService
|
||||
rag_service = RAGService(
|
||||
model_name=scoring_config['rag_model'],
|
||||
vector_store=vector_store,
|
||||
cache_dir=cache_dir,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
logger.info(f"RAGService инициализирован: {scoring_config['rag_model']}")
|
||||
|
||||
# Инициализация DeepSeek сервиса
|
||||
deepseek_service = None
|
||||
if scoring_config['deepseek_enabled'] and scoring_config['deepseek_api_key']:
|
||||
deepseek_service = DeepSeekService(
|
||||
api_key=scoring_config['deepseek_api_key'],
|
||||
api_url=scoring_config['deepseek_api_url'],
|
||||
model=scoring_config['deepseek_model'],
|
||||
timeout=scoring_config['deepseek_timeout'],
|
||||
enabled=True,
|
||||
)
|
||||
logger.info(f"DeepSeekService инициализирован: {scoring_config['deepseek_model']}")
|
||||
|
||||
# Создаем менеджер
|
||||
self._scoring_manager = ScoringManager(
|
||||
rag_service=rag_service,
|
||||
deepseek_service=deepseek_service,
|
||||
)
|
||||
|
||||
return self._scoring_manager
|
||||
|
||||
def get_scoring_manager(self):
|
||||
"""
|
||||
Возвращает ScoringManager для ML-скоринга постов.
|
||||
|
||||
Инициализируется лениво при первом вызове.
|
||||
|
||||
Returns:
|
||||
ScoringManager или None если скоринг полностью отключен
|
||||
"""
|
||||
if self._scoring_manager is None:
|
||||
scoring_config = self.settings.get('Scoring', {})
|
||||
|
||||
# Проверяем, включен ли хотя бы один сервис
|
||||
rag_enabled = scoring_config.get('rag_enabled', False)
|
||||
deepseek_enabled = scoring_config.get('deepseek_enabled', False)
|
||||
|
||||
if not rag_enabled and not deepseek_enabled:
|
||||
logger.info("Scoring полностью отключен (RAG и DeepSeek disabled)")
|
||||
return None
|
||||
|
||||
self._init_scoring_manager()
|
||||
|
||||
return self._scoring_manager
|
||||
|
||||
|
||||
_global_instance = None
|
||||
|
||||
@@ -111,7 +111,16 @@ def determine_anonymity(post_text: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_text_message(post_text: str, first_name: str, username: str = None, is_anonymous: Optional[bool] = None):
|
||||
def get_text_message(
|
||||
post_text: str,
|
||||
first_name: str,
|
||||
username: str = None,
|
||||
is_anonymous: Optional[bool] = None,
|
||||
deepseek_score: Optional[float] = None,
|
||||
rag_score: Optional[float] = None,
|
||||
rag_confidence: Optional[float] = None,
|
||||
rag_score_pos_only: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон"
|
||||
или переданного параметра is_anonymous.
|
||||
@@ -121,6 +130,10 @@ def get_text_message(post_text: str, first_name: str, username: str = None, is_a
|
||||
first_name: Имя автора поста
|
||||
username: Юзернейм автора поста (может быть None)
|
||||
is_anonymous: Флаг анонимности (True - анонимно, False - не анонимно, None - legacy, определяется по тексту)
|
||||
deepseek_score: Скор от DeepSeek API (0.0-1.0, опционально)
|
||||
rag_score: Скор от RAG/ruBERT neg/pos (0.0-1.0, опционально)
|
||||
rag_confidence: Уверенность RAG модели (0.0-1.0, зависит от количества примеров)
|
||||
rag_score_pos_only: Скор RAG только по положительным примерам (0.0-1.0, опционально)
|
||||
|
||||
Returns:
|
||||
str: - Сформированный текст сообщения.
|
||||
@@ -137,21 +150,37 @@ def get_text_message(post_text: str, first_name: str, username: str = None, is_a
|
||||
else:
|
||||
author_info = f"{first_name} (Ник не указан)"
|
||||
|
||||
# Формируем базовый текст
|
||||
# Если передан is_anonymous, используем его, иначе определяем по тексту (legacy)
|
||||
# TODO: Уверен можно укоротить
|
||||
if is_anonymous is not None:
|
||||
if is_anonymous:
|
||||
return f'{safe_post_text}\n\nПост опубликован анонимно'
|
||||
final_text = f'{safe_post_text}\n\nПост опубликован анонимно'
|
||||
else:
|
||||
return f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
final_text = f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
else:
|
||||
# Legacy: определяем по тексту
|
||||
if "неанон" in post_text or "не анон" in post_text:
|
||||
return f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
final_text = f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
elif "анон" in post_text:
|
||||
return f'{safe_post_text}\n\nПост опубликован анонимно'
|
||||
final_text = f'{safe_post_text}\n\nПост опубликован анонимно'
|
||||
else:
|
||||
return f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
final_text = f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
|
||||
# Добавляем блок со скорами если есть
|
||||
if deepseek_score is not None or rag_score is not None or rag_score_pos_only is not None:
|
||||
scores_lines = ["\n📊 Уверенность в одобрении:"]
|
||||
if deepseek_score is not None:
|
||||
scores_lines.append(f"DeepSeek: {deepseek_score:.2f}")
|
||||
if rag_score is not None:
|
||||
rag_line = f"RAG neg/pos: {rag_score:.2f}"
|
||||
if rag_confidence is not None:
|
||||
rag_line += f" (уверенность: {rag_confidence:.0%})"
|
||||
scores_lines.append(rag_line)
|
||||
if rag_score_pos_only is not None:
|
||||
scores_lines.append(f"RAG pos only: {rag_score_pos_only:.2f}")
|
||||
final_text += "\n" + "\n".join(scores_lines)
|
||||
|
||||
return final_text
|
||||
|
||||
@track_time("download_file", "helper_func")
|
||||
@track_errors("helper_func", "download_file")
|
||||
|
||||
Reference in New Issue
Block a user