Files
tg-digest/summarizer.py
Тимур Абайдулин 9fd7d42c6a init
2026-02-07 14:46:15 +03:00

151 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Суммаризация новостей через 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"]