""" Суммаризация новостей через LLM. Поддержка: Anthropic Claude, OpenAI, Ollama (нативный на хосте). """ import logging import httpx logger = logging.getLogger(__name__) SYSTEM_PROMPT = """ВАЖНО: Весь ответ ДОЛЖЕН быть написан ТОЛЬКО на {language} языке. Никакого английского. Ты — опытный редактор новостного дайджеста. Твоя задача: 1. Прочитай все собранные сообщения из Telegram-каналов. 2. Сгруппируй новости по темам (политика, экономика, технологии, общество и т.д.). 3. Для каждой темы напиши краткое резюме ключевых событий. 4. Если несколько каналов пишут об одном событии — объедини в одну новость, не дублируй. 5. Выдели 3-5 главных новостей дня отдельным блоком в начале. 6. Для каждой новости укажи источник (название канала). Формат ответа: 📌 **ГЛАВНОЕ** • [краткая новость] — _Источник_ ... 📂 **[ТЕМА]** • [краткая новость] — _Источник_ ... Правила: - Пиши кратко и по делу, без воды - Не выдумывай информацию — только то, что есть в сообщениях - Если сообщение — реклама или спам, пропусти его - Сохраняй нейтральный тон - Названия тем, заголовки и весь текст — ТОЛЬКО на {language} языке """ def _build_messages_block(messages: list) -> str: parts = [] for i, msg in enumerate(messages, 1): date_str = msg.date.strftime("%Y-%m-%d %H:%M") part = ( f"--- Сообщение {i} ---\n" f"Канал: {msg.channel_title}\n" f"Дата: {date_str}\n" f"Просмотры: {msg.views}\n" f"Текст:\n{msg.text}\n" ) if msg.url: part += f"Ссылка: {msg.url}\n" parts.append(part) return "\n".join(parts) def _truncate_to_fit(text: str, max_chars: int = 180_000) -> str: if len(text) <= max_chars: return text logger.warning(f"Текст обрезан: {len(text)} → {max_chars} символов") return text[:max_chars] + "\n\n[...текст обрезан из-за лимита контекста]" class Summarizer: def __init__(self, config: dict): self.cfg = config["llm"] self.provider = self.cfg["provider"] self.language = self.cfg.get("language", "русский") self.max_tokens = self.cfg.get("max_tokens", 4096) async def summarize(self, messages: list) -> str: if not messages: return "📭 Нет новых сообщений за указанный период." messages_block = _build_messages_block(messages) messages_block = _truncate_to_fit(messages_block) user_prompt = ( f"Вот {len(messages)} сообщений из Telegram-каналов. " f"Составь новостной дайджест на {self.language} языке. " f"Весь текст ответа должен быть на {self.language} языке.\n\n" f"{messages_block}" ) system = SYSTEM_PROMPT.format(language=self.language) logger.info( f"Суммаризация через {self.provider} " f"({len(messages)} сообщений, ~{len(messages_block)} символов)" ) if self.provider == "anthropic": return await self._anthropic(system, user_prompt) elif self.provider == "openai": return await self._openai(system, user_prompt) elif self.provider == "ollama": return await self._ollama(system, user_prompt) else: raise ValueError(f"Неизвестный провайдер: {self.provider}") async def _anthropic(self, system: str, user_prompt: str) -> str: import anthropic client = anthropic.AsyncAnthropic( api_key=self.cfg["anthropic_api_key"] ) response = await client.messages.create( model=self.cfg.get("anthropic_model", "claude-sonnet-4-20250514"), max_tokens=self.max_tokens, system=system, messages=[{"role": "user", "content": user_prompt}], ) return response.content[0].text async def _openai(self, system: str, user_prompt: str) -> str: from openai import AsyncOpenAI client = AsyncOpenAI(api_key=self.cfg["openai_api_key"]) response = await client.chat.completions.create( model=self.cfg.get("openai_model", "gpt-4o-mini"), max_tokens=self.max_tokens, messages=[ {"role": "system", "content": system}, {"role": "user", "content": user_prompt}, ], ) return response.choices[0].message.content async def _ollama(self, system: str, user_prompt: str) -> str: url = self.cfg.get("ollama_url", "http://host.docker.internal:11434") model = self.cfg.get("ollama_model", "gemma3:12b") async with httpx.AsyncClient(timeout=300) as client: response = await client.post( f"{url}/api/chat", json={ "model": model, "stream": False, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user_prompt}, ], }, ) response.raise_for_status() return response.json()["message"]["content"]