Files
tg-digest-docker/summarizer.py
Тимур Абайдулин ba1245a06c init
2026-02-07 16:31:46 +03:00

148 lines
6.0 KiB
Python
Raw Permalink 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
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"]