Создание персонального новостного агрегатора часто упирается либо в стоимость готовых решений (Perplexity, ChatGPT Plus), либо в необходимость администрирования собственного хостинга (VPS). Однако для задач периодического мониторинга и суммаризации контента достаточно бесплатных инструментов: Google Gemini API (Free Tier) и GitHub Actions.

На реальном примере рассказываю про свой опыт и реализацию автономного бота, который работает без постоянного сервера, парсит RSS-ленты по расписанию, структурирует информацию с помощью LLM и публикует готовые посты в Telegram.

Архитектура решения

Классические Telegram-боты обычно работают в режиме polling (постоянный опрос сервера), что требует запущенного процесса 24/7. Для новостного агрегатора это избыточно. Оптимальный подход — CRON-скрипт.

Схема работы:

  1. Scheduler: GitHub Actions запускает контейнер по расписанию (например, 4 раза в сутки).

  2. Collector: Python-скрипт собирает заголовки из RSS-источников.

  3. Processor: Gemini Flash обрабатывает сырые данные, фильтрует и формирует текст.

  4. Publisher: Готовый контент отправляется через Telegram Bot API.

  5. Termination: Контейнер уничтожается до следующего запуска.

Жизненный цикл одного запуска скрипта: от триггера до уничтожения контейнера.
Жизненный цикл одного запуска скрипта: от триггера до уничтожения контейнера.

Это позволяет уложиться в бесплатные лимиты GitHub Actions и Google AI Studio.

1. Временные слоты и конфигурация

Чтобы бот не превратился в спам-машину, логика вещания привязывается ко времени суток. Скрипт определяет текущий час и выбирает соответствующий профиль: утренний дайджест, дневной лонгрид или вечерняя аналитика.

В коде это реализуется через словарь конфигурации. Функция get_current_config возвращает целевые URL и режим промпта в зависимости от часа запуска.

def get_current_config():
    hour = datetime.utcnow().hour
    # Логика слотов (UTC): 6 утра, 10, 13 и 16 часов
    target_slot = 6 
    
    if 9 <= hour < 12: target_slot = 10 
    elif 12 <= hour < 15: target_slot = 13
    elif 15 <= hour < 20: target_slot = 16

    CONFIG = {
        6: { # Утро: короткий дайджест или рынки
            "topic": "Главное к утру",
            "mode": random.choice(["DIGEST", "MARKETS"]), 
            "urls": ["https://api.axios.com/feed/", "https://cointelegraph.com/feed"]
        },
        10: { # Обед: разбор технологии
            "topic": "Технологии",
            "mode": "ESSAY",
            "urls": ["https://techcrunch.com/feed/", "https://www.theverge.com/rss/index.xml"]
        }
        # ... остальные слоты
    }
    return CONFIG[target_slot]

2. Агрегация данных

Для взаимодействия с LLM не обязательно скачивать полные тексты статей. Gemini обладает достаточным контекстным окном, но для экономии токенов и ускорения работы эффективнее скармливать модели список заголовков с ссылками.

Тут использую библиотеку feedparser. Собранные заголовки перемешиваются, чтобы избежать перекоса в сторону одного источника, если в ленте много новостей.

def get_combined_news(url_list):
    all_entries = []
    for url in url_list:
        # Парсинг RSS и формирование строки вида [Источник] Заголовок
        feed = feedparser.parse(requests.get(url).content)
        for entry in feed.entries:
            clean_link = entry.link.split("?")[0]
            all_entries.append(f"- [{urlparse(clean_link).netloc}] {entry.title} ||| {clean_link}")
    
    random.shuffle(all_entries)
    return "\n".join(all_entries[:30]) # Ограничение объема на вход

3. Инжиниринг промптов

Качество выхода напрямую зависит от жесткости заданных ограничений. Просто попросить «пересказать новости» приведет к потере фактуры. В скрипте используются разные системные промпты для разных режимов.

Ключевые требования к промпту:

  1. Ролевая модель: «Ты редактор», «Ты финансовый аналитик».

  2. Структура HTML: Telegram требует закрытых тегов. Модель инструктируется вставлять ссылки в формате <a href='url'>источник</a>.

  3. Запрет на выдумки: Строгое следование переданному списку новостей.

def process_with_gemini(news_text, config):
    mode = config['mode']
    
    # Общие правила форматирования для всех режимов
    SHARED_RULES = (
        "ТОН: Спокойный, без маркетинга.\n"
        "ССЫЛКИ: Встраивать в текст через тег <a href>.\n"
    )

    if mode == "ESSAY":
        prompt = (
            f"Роль: Редактор. Тема: {config['topic']}.\n"
            "СТРУКТУРА (3 абзаца):\n"
            "1. Заголовок (жирным).\n"
            "2. Инфоповод: Что случилось + ссылка.\n"
            "3. Контекст: Почему это важно.\n"
            f"ЛЕНТА:\n{news_text}"
        )
    elif mode == "DIGEST":
         prompt = "..." # Инструкция для списка событий

    model = genai.GenerativeModel('gemini-flash-latest')
    result = model.generate_content(prompt)
    return result.text

4. Взаимодействие с Telegram API

Telegram имеет ограничение на длину сообщения с медиа-вложением (caption) — 1024 символа. Если LLM сгенерирует длинный анализ, запрос sendPhoto вернет ошибку.

Реализована логика раздельной отправки:

  1. Если текст короче 950 символов (оставляем запас на теги) — отправляется картинка с подписью.

  2. Если текст длиннее — сначала отправляется изображение, следом идет текстовое сообщение.

Картинка парсится из OpenGraph-тегов исходной статьи (если режим подразумевает фокус на одной новости).

def send_to_telegram(raw_text, t_token, c_id):
    # Логика определения длины
    is_long_text = len(raw_text) > 950 
    
    if file_bytes and is_long_text:
        # Раздельная отправка
        requests.post(url_photo, files=..., data={'chat_id': c_id})
        requests.post(url_message, data={'chat_id': c_id, 'text': raw_text})
    elif file_bytes:
        # Фото с подписью
        requests.post(url_photo, files=..., data={'caption': raw_text})
    else:
        # Только текст
        requests.post(url_message, data={'text': raw_text})
Пример генерации в режиме ESSAY: структурированный текст с сохранением ссылок на источники.
Пример генерации в режиме ESSAY: структурированный текст с сохранением ссылок на источники.

Финальный этап — автоматизация. Создается Workflow-файл .github/workflows/main.yml.

Секреты (API ключи Telegram и Google) хранятся в настройках репозитория (Settings -> Secrets). Крон настраивается со смещением минут (например, 15-я минута часа), чтобы избежать очередей на раннерах GitHub в начале часа.

name: News Bot

on:
  schedule:
    # Запуск в 06:15, 10:15, 13:15, 16:15 (UTC)
    - cron: '15 6,10,13,16 * * *'
  workflow_dispatch: # Ручной запуск

jobs:
  run-bot:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'
      - name: Install dependencies
        run: pip install requests feedparser beautifulsoup4 google-generativeai
      - name: Run Script
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHANNEL_ID: ${{ secrets.TELEGRAM_CHANNEL_ID }}
          GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
        run: python main.py

Заключение

Реализованная схема позволяет получить полностью бесплатный, необслуживаемый инструмент для мониторинга инфополя. Отсутствие состояния (stateless) упрощает отладку: каждый запуск происходит в чистом окружении. Ключевым преимуществом выстраивания пайплайна вокруг Google Gemini являются высокие лимиты Free Tier и широкое контекстное окно, достаточное для обработки массивных RSS-лент за одну итерацию.

Демонстрация работы описанного алгоритма в реальном времени доступна в Telegram-канале. https://t.me/explainer_news.

Спасибо, что дочитали.