This commit is contained in:
Тимур Абайдулин
2026-02-07 14:46:15 +03:00
commit 9fd7d42c6a
8 changed files with 698 additions and 0 deletions

150
summarizer.py Normal file
View File

@@ -0,0 +1,150 @@
"""
Суммаризация новостей через LLM.
Поддержка: Anthropic Claude, OpenAI, Ollama (локальный).
"""
import logging
from datetime import datetime, timezone
import httpx
logger = logging.getLogger(__name__)
SYSTEM_PROMPT = """Ты — опытный редактор новостного дайджеста. Твоя задача:
1. Прочитай все собранные сообщения из Telegram-каналов.
2. Сгруппируй новости по темам (политика, экономика, технологии, общество и т.д.).
3. Для каждой темы напиши краткое резюме ключевых событий.
4. Если несколько каналов пишут об одном событии — объедини в одну новость, не дублируй.
5. Выдели 3-5 главных новостей дня отдельным блоком в начале.
6. Для каждой новости укажи источник (название канала).
Формат ответа:
📌 **ГЛАВНОЕ**
• [краткая новость] — _Источник_
...
📂 **[ТЕМА]**
• [краткая новость] — _Источник_
...
Правила:
- Пиши кратко и по делу, без воды
- Язык: {language}
- Не выдумывай информацию — только то, что есть в сообщениях
- Если сообщение — реклама или спам, пропусти его
- Сохраняй нейтральный тон
"""
def _build_messages_block(messages: list) -> str:
"""Форматирует сообщения в текстовый блок для LLM."""
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"Составь новостной дайджест.\n\n{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://localhost:11434")
model = self.cfg.get("ollama_model", "llama3.1:8b")
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"]