""" Topics Analyzer — анализ тем чата, облако тегов Классификация сообщений по темам и категориям """ import logging import re from datetime import datetime, timedelta from typing import Dict, List, Tuple, Optional from collections import defaultdict, Counter from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) class TopicsAnalyzer: """Анализатор тем чата""" def __init__(self, session: AsyncSession): self.session = session self.topics = self._load_topics() def _load_topics(self) -> Dict: """Загрузить темы и ключевые слова""" return { 'лифты': { 'keywords': ['лифт', 'лифты', 'лифтовая', 'кабина', 'шахта лифта', 'лифт застрял', 'лифт не работает'], 'emoji': '🛗' }, 'парковка': { 'keywords': ['парковк', 'машина', 'автомобиль', 'место парковк', 'припаркован', 'шлагбаум'], 'emoji': '🚗' }, 'уборка': { 'keywords': ['уборк', 'грязн', 'мусор', 'подъезд', 'лестниц', 'пол', 'пыль'], 'emoji': '🧹' }, 'соседи': { 'keywords': ['сосед', 'соседи', 'шум', 'громко', 'музыка', 'ремонт', 'сверлит'], 'emoji': '👥' }, 'вода': { 'keywords': ['вод', 'кран', 'труб', 'протечк', 'засор', 'канализаци', 'стояк'], 'emoji': '💧' }, 'электричество': { 'keywords': ['свет', 'электричеств', 'розетк', 'проводк', 'автомат', 'лампочк', 'мигает'], 'emoji': '⚡' }, 'отопление': { 'keywords': ['отоплен', 'батаре', 'радиатор', 'тепло', 'холод', 'температур'], 'emoji': '🔥' }, 'домофон': { 'keywords': ['домофон', 'ключ', 'карта', 'доступ', 'вход', 'дверь'], 'emoji': '🔑' }, 'мусор': { 'keywords': ['мусор', 'контейнер', 'баки', 'вывоз', 'раздельный', 'переработк'], 'emoji': '🗑️' }, 'дети': { 'keywords': ['ребенок', 'дети', 'детск', 'коляск', 'площадк', 'песочниц'], 'emoji': '👶' }, 'животные': { 'keywords': ['собак', 'кошк', 'животн', 'питомец', 'лают', 'мяукают'], 'emoji': '🐕' }, 'объявления': { 'keywords': ['объявлен', 'продам', 'куплю', 'отдам', 'ищу', 'предлагаю'], 'emoji': '📢' }, 'жалобы': { 'keywords': ['жалоб', 'жалуюсь', 'недовольн', 'возмущен', 'требую', 'безобраз'], 'emoji': '😠' }, 'благодарности': { 'keywords': ['спасиб', 'благодар', 'благодарю', 'молодец', 'хорошо', 'отличн'], 'emoji': '🙏' }, 'вопросы': { 'keywords': ['вопрос', 'подскажит', 'помогит', 'кто знает', 'как сделать'], 'emoji': '❓' }, 'предложения': { 'keywords': ['предлагаю', 'предложен', 'давайте', 'может быть', 'идея'], 'emoji': '💡' }, 'собрание': { 'keywords': ['собран', 'встреч', 'совещан', 'голосован', 'опрос', 'обсужден'], 'emoji': '📅' }, 'ук': { 'keywords': ['ук', 'управляющ', 'компан', 'жкх', 'паспортист', 'диспетчер'], 'emoji': '🏢' }, } async def analyze_message(self, text: str) -> Optional[str]: """ Классифицировать сообщение по теме Returns: название темы или None """ if not text: return None text_lower = text.lower() topic_scores = defaultdict(int) for topic_name, topic_data in self.topics.items(): for keyword in topic_data['keywords']: if keyword in text_lower: topic_scores[topic_name] += 1 if not topic_scores: return None # Возвращаем тему с наибольшим score best_topic = max(topic_scores, key=topic_scores.get) return best_topic async def get_topics_stats(self, days: int = 7) -> Dict: """ Получить статистику тем за период Returns: dict с темами и количеством сообщений """ from database.models import Message time_window = datetime.utcnow() - timedelta(days=days) stmt = ( select(Message.text) .where(Message.timestamp >= time_window) .where(Message.text.isnot(None)) ) result = await self.session.execute(stmt) messages = [row[0] for row in result.all()] topic_counts = defaultdict(int) for text in messages: topic = await self.analyze_message(text) if topic: topic_counts[topic] += 1 # Считаем общее количество классифицированных сообщений total = sum(topic_counts.values()) unclassified = len(messages) - total # Формируем результат result = { 'total_messages': len(messages), 'classified': total, 'unclassified': unclassified, 'topics': {} } for topic_name, count in sorted(topic_counts.items(), key=lambda x: x[1], reverse=True): percent = (count / total * 100) if total > 0 else 0 emoji = self.topics[topic_name]['emoji'] result['topics'][topic_name] = { 'count': count, 'percent': round(percent, 1), 'emoji': emoji } return result async def get_topics_cloud(self, days: int = 7) -> str: """ Получить облако тегов в текстовом виде Returns: форматированный текст """ stats = await self.get_topics_stats(days) if not stats['topics']: return 'ℹ️ Нет данных за указанный период' text = f'☁️ Облако тем чата (за {days} дн.)\n\n' text += f'Всего сообщений: {stats["total_messages"]}\n' text += f'Распознано: {stats["classified"]} ({stats["classified"]/stats["total_messages"]*100:.0f}%)\n\n' # Формируем облако sorted_topics = sorted( stats['topics'].items(), key=lambda x: x[1]['percent'], reverse=True ) for topic_name, data in sorted_topics: bar_length = int(data['percent'] / 5) # 1 символ = 5% bar = '█' * bar_length + '░' * (20 - bar_length) text += f'{data["emoji"]} {topic_name.capitalize()}: {bar} {data["percent"]}% ({data["count"]})\n' return text async def get_top_topics(self, days: int = 7, limit: int = 5) -> List[Tuple[str, Dict]]: """ Получить топ тем за период Returns: список (название_темы, данные) """ stats = await self.get_topics_stats(days) sorted_topics = sorted( stats['topics'].items(), key=lambda x: x[1]['percent'], reverse=True ) return sorted_topics[:limit] async def get_topic_trend(self, topic: str, days: int = 14) -> Dict: """ Получить тренд темы по дням Returns: dict с датами и количеством сообщений """ from database.models import Message time_window = datetime.utcnow() - timedelta(days=days) stmt = ( select(Message.text, func.date(Message.timestamp).label('msg_date')) .where(Message.timestamp >= time_window) .where(Message.text.isnot(None)) ) result = await self.session.execute(stmt) messages = [(row[0], row[1]) for row in result.all()] daily_counts = defaultdict(int) for text, date in messages: topic = await self.analyze_message(text) if topic == topic: daily_counts[date] += 1 return { 'topic': topic, 'days': days, 'daily_counts': dict(sorted(daily_counts.items())), 'total': sum(daily_counts.values()) } async def get_weekly_report(self) -> str: """ Сформировать еженедельный отчёт по темам Returns: форматированный текст отчёта """ stats = await self.get_topics_stats(7) prev_stats = await self.get_topics_stats(14) text = '📊 Еженедельный отчёт по темам чата\n\n' text += f'Период: последние 7 дней\n\n' # Топ тем text += '🔥 Топ тем недели:\n\n' top_topics = await self.get_top_topics(7, 5) for i, (topic_name, data) in enumerate(top_topics, 1): text += f'{i}. {data["emoji"]} {topic_name.capitalize()}: {data["percent"]}% ({data["count"]} сообщ.)\n' # Сравнение с прошлой неделей text += '\n📈 Динамика:\n\n' if prev_stats['topics']: for topic_name, data in stats['topics'].items(): prev_data = prev_stats['topics'].get(topic_name, {'count': 0}) diff = data['count'] - prev_data['count'] if diff > 0: text += f'↗️ {topic_name.capitalize()}: +{diff} ({data["emoji"]})\n' elif diff < 0: text += f'↘️ {topic_name.capitalize()}: {diff} ({data["emoji"]})\n' text += '\n💡 Используйте /topics для детальной статистики' return text