Skip to content

Latest commit

 

History

62 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NVIDIA Telegram Bot

Telegram-бот на моделях NVIDIA API со стримингом ответов и предпросмотром прямо в сообщении. По умолчанию — Nemotron 3 Super 120B; модель переключается прямо в чате через /menu.

Возможности

  • Показывает процесс «думания» поэтапно (таймер + предпросмотр ответа)
  • Streaming-режим (по токенам)
  • Помнит контекст диалога (полная память в личке, короткая в группах)
  • Выбор провайдера и модели через меню (/menu)
  • Поиск в интернете перед ответом: модель сама решает, когда искать, и приводит источники (нужен TAVILY_API_KEY)
  • Викторины (/quiz), умные опросы (/poll), помощь с кодом (/code), задачи (/task)
  • Техсобеседование по Go: /interview — вопросы с реальных собеседований (база в interview.json), режимы «тест с вариантами» и «напиши сам» + разбор правильного ответа
  • Путь/roadmap по стеку и языкам: /roadmap — уровни с конкретными языками, продвижение и отметка тем
  • Настройка стека: /stack (или кнопка «🧰 Стек и языки») — выбрать готовый шаблон или собрать свой стек из языков
  • Режим обучения: /learn (или кнопка «🎓 Режим обучения») — бот высылает теорию по уровню, затем тест-викторину и проверяет знания по каждому ответу
  • Экспорт для Obsidian: /export — скачивает весь roadmap + сгенерированные задачи/вопросы одним .md файлом
  • Прогресс, выбор стека и выбранная модель сохраняются в progress.json (переживают перезапуск)
  • При отказе модели (rate limit, таймаут, недоступность) бот повторяет запрос, затем автоматически переходит на запасную модель из меню и помечает это в ответе
  • Групповые команды: /context, /summary, /judge
  • Работает в личке, по @упоминанию и по reply в группах

Доступные модели

Скорость замерена одним и тем же промптом в одном окне, поэтому цифры сравнимы между собой:

ключ в /menu модель ток/с первый токен
super-120b (по умолчанию) nvidia/nemotron-3-super-120b-a12b 115 1.3с
lightning-30b nvidia/nemotron-3.5-lightning-30b-a3b 149 4.0с
ultra-550b nvidia/nemotron-3-ultra-550b-a55b 47 3.4с
inkling thinkingmachines/inkling 36 7.5с
minimax-m3 minimaxai/minimax-m3 7–17 2.7с
deepseek-flash deepseek-ai/deepseek-v4-flash-0731 не замерено не замерено

Порядок в таблице — не украшение. Первый ключ идёт моделью по умолчанию, а следующие за текущей уходят в запасные (_fallback_models берёт следующие MAX_FALLBACKS), поэтому сразу за super-120b стоят два самых быстрых варианта: подмена модели не должна оборачиваться ожиданием.

Рассуждающих моделей в списке нет намеренно. Такая модель молчит, пока думает, и первый токен приходит через десятки секунд — в чате это неотличимо от зависшего бота. Скорость конкретной модели удобно замерить скриптом test_flash.py, он же покажет, как она раскладывает поток по полям:

python test_flash.py nvidia/nemotron-3-super-120b-a12b

deepseek-flash — с неё бот и начинался, потом её сменили на Qwen. Размышления у DeepSeek V4 включаются явно (chat_template_kwargs: {thinking: true}), то есть по умолчанию модель отвечает сразу, без прохода размышлений. В списке стоит последней намеренно: пока она не проверена на текущем каталоге, в запасные модели не попадает (_fallback_models берёт первые MAX_FALLBACKS), но в /menu выбирается. Проверить, что ключ её видит:

python test_models.py deepseek-ai/deepseek-v4-flash-0731

Пропускная способность публичного эндпоинта плавает в зависимости от нагрузки, так что конкретные значения будут отличаться — важен порядок величин. Список моделей задаётся словарём PROVIDERS["nvidia"]["models"] в main.py, туда же можно добавить любую другую модель из каталога (см. list_models.py).

Откуда берётся время ответа

Ответ ждут в чате, поэтому в коде есть места, специально сделанные ради скорости. Ломать их «заодно» не стоит:

  • Модель по умолчанию — не рассуждающая. Это самый крупный вклад: рассуждающая молчит перед первым токеном столько же, сколько потом печатает ответ.
  • Один общий httpx.AsyncClient на процесс (http_client() в main.py, client() в research.py). Раньше клиент создавался на каждый запрос, то есть на каждое обращение к модели приходилось полное TLS-рукопожатие, а на один детальный вопрос их уходит до восьми. Теперь соединения переиспользуются.
  • История в промпте обрезается. Она уходит в модель дважды за сообщение (маршрутный вызов + ответ), и десять развёрнутых ответов — это десятки тысяч токенов префилла перед первым токеном. Последний обмен идёт целиком, ответы постарше режутся до MAX_OLD_REPLY_CHARS.
  • Заглушку с таймером правит одна фоновая задача. Цикл чтения токенов только копит куски: пока он ждал ответа от Telegram на свою правку, он не вычитывал поток от модели. Просыпается задача по событию, а не по sleep, иначе готовый ответ ждал конца её сна — до двух лишних секунд на каждом ответе.
  • Таймауты разнесены по фазам (request_timeout): соединение обязано устанавливаться за CONNECT_TIMEOUT, а STREAM_TIMEOUT отмеряется между порциями потока. Одно общее число держало зависший коннект столько же, сколько живую генерацию.

Потолки времени задаются переменными окружения — см. .env.example.

Веб-поиск и актуальность

У любой модели есть отсечка знаний, поэтому без поиска она уверенно выдаёт устаревшие данные за свежие. Чтобы этого не происходило:

  • в промпт подставляется сегодняшняя дата и указание опираться на поиск, а не на память;
  • бот описывает модели инструмент web_search, и та сама решает, когда им воспользоваться. В описании инструмента явно перечислено, когда искать НЕ надо: теория, код, отладка, математика, переводы, определения, болтовня — на это модель отвечает сама. Незнакомое слово тоже не повод для поиска: бот скажет, что не знает термина, и переспросит. Проверяется скриптом test_search_trigger.py (14 случаев, бьёт по живому API);
  • за один вызов модель присылает 2-4 разные формулировки одного вопроса; они уходят в Tavily параллельно, выдача сливается с дедупликацией по URL;
  • поиск идёт до 4 кругов: модель уточняет формулировки и углубляется в детали. Формулировки, уже отправленные в поиск, повторно не ищутся;
  • пока идёт поиск, в сообщении построчно копится ход работы: 🔍 запросы и 📄 сайты, на которые бот заходит. Правки сообщения троттлятся (не чаще раза в 1.2с), иначе Telegram включает флуд-контроль;
  • получив выдачу, модель сверяет факты между источниками: официальным источникам (документация, релиз-ноуты) веса больше, чем блогам и агрегаторам, а расхождения она обязана назвать прямо;
  • последней строкой ответа идёт оценка: «Достоверность: высокая/средняя/низкая — почему»;
  • источники указываются компактной строкой доменов после текста, без отдельного заголовка.

Реализация — в research.py, он не зависит от main.py и получает вызов модели колбэком. Ограничители заданы константами там же: MAX_ROUNDS, MAX_RESULTS, MAX_CHARS_TOTAL.

Без TAVILY_API_KEY шаг поиска пропускается целиком и бот работает как раньше — прод не сломается, если ключ забыли прописать в окружении.

Шаг «решить, нужен ли интернет» стоит отдельного обращения к модели — она отвечает вызовом инструмента вместо текста. На приветствиях и благодарностях он пропускается: список в SMALL_TALK сверяется с репликой целиком, так что «привет, найди курс биткоина» по-прежнему идёт через поиск.

Настройка сделана в пользу полноты, а не скорости: ответ подробный и структурированный, разбитый на разделы с таблицами. На практике вопрос вроде «что нового в Go в 2026» занимает 2-3 минуты и даёт около 9000 символов текста.

Ответ всегда приходит одним сообщением. У Telegram лимит 4096 символов, и при превышении он отвергает сообщение целиком, поэтому длинный ответ публикуется на telegra.ph — в чате остаётся начало и ссылка «Читать полностью», которая раскрывается в клиенте окном Instant View.

Таблицы

Ни Telegram, ни Telegraph не поддерживают <table>, поэтому таблицы рисуются моноширинным блоком. Модуль tables.py выравнивает колонки по ширине и ставит линию под заголовком:

Версия │ Дата       │ GC
───────┼────────────┼──────────
1.26   │ 2026-02-10 │ Green Tea

Если таблица шире экрана (на телефоне это ~42 моноширинных символа), она разворачивается в список «поле: значение» — иначе строки переносятся и вёрстка разъезжается:

▸ Генерические методы
   Описание: Методы могут иметь собственные типовые параметры
   Источник: go.dev

Код в ячейках — отдельная история. Многострочная ячейка в markdown невозможна, поэтому модель записывает переносы escape-последовательностями, и без обработки пример кода приезжает одной нечитаемой строкой с и \". Такой код вынимается из ячейки и печатается настоящими строками, а таблица принудительно идёт списком: выровнять многострочную ячейку нельзя. В промпте модель вдобавок просят выносить код отдельным блоком и не помещать его в таблицы.

Готовые блоки <pre> прячутся от markdown-конвертера и от финальной чистки пробелов (re.sub(r' * *', ...)) — обе срезают отступы в начале строк, на которых держится вёрстка.

Иллюстрации

Tavily вместе с выдачей отдаёт картинки с текстовыми описаниями (include_images + include_image_descriptions). Описания попадают в результаты поиска каталогом, и модель сама расставляет маркеры [imgN] там, где картинка уместна — она видит описание и отсеивает декоративное (на проверке пропустила «мультяшного хомяка», взяв баннеры релиза).

Картинки живут только на странице Telegraph: в сообщении Telegram изображений в тексте быть не может, поэтому оттуда маркеры вырезаются. Одна картинка показывается один раз, несуществующие номера игнорируются.

Telegraph принимает контент только массивом узлов (на сырой HTML отвечает CONTENT_FORMAT_INVALID), поэтому в telegraph.py лежит свой конвертер markdown → узлы: заголовки, списки, блоки кода, ссылки; таблиц у Telegraph нет, они уходят в <pre>. API публичный, ключ не нужен — аккаунт создаётся на лету. Свой можно зафиксировать переменной TELEGRAPH_TOKEN.

Видимая часть подбирается замером: md_to_html раздувает markdown неравномерно (сильнее всего жирный шрифт), поэтому кусок ужимается, пока не влезет вместе с заголовком, ссылкой и списком источников.

Если полнота важнее времени — крутите MAX_ROUNDS, MAX_QUERIES и MAX_RESULTS в research.py вверх; если наоборот, вниз. Поэтому поиск и отдан на усмотрение модели, а не включён на каждое сообщение.

Установка

cd numbertree_bot

python -m venv venv
venv\Scripts\activate  # Windows
# source venv/bin/activate  # Linux/Mac

pip install -r requirements.txt

cp .env.example .env
# Отредактируйте .env и вставьте ваши токены

Получение токенов

Telegram Bot Token

  1. Напишите @BotFather
  2. Отправьте /newbot
  3. Следуйте инструкциям
  4. Скопируйте токен в .env как TELEGRAM_BOT_TOKEN

NVIDIA API Key

  1. Зарегистрируйтесь на build.nvidia.com
  2. Создайте API Key (начинается с nvapi-)
  3. Скопируйте в .env как NVIDIA_API_KEY

Endpoint (OpenAI-совместимый) уже настроен в коде: https://integrate.api.nvidia.com/v1

Если ходите через свой прокси, переопределите базовый адрес переменной NVIDIA_API_BASE.

Проверить, какие модели доступны вашему ключу:

python list_models.py minimax   # фильтр по подстроке
python list_models.py --all     # весь каталог

Запуск

python main.py

Тесты

Что и когда чинилось — в CHANGELOG.md.

Офлайн, без ключей и без обращений к API:

python test_regressions.py        # разметка, счёт собеседования, настройки, команды
python test_interview_routing.py  # маршрутизация текста во время интервью

Остальные test_*.py — разведочные скрипты по живому API: им нужен NVIDIA_API_KEY, и они тратят запросы.

Премиальные эмодзи <tg-emoji> доступны только ботам с дополнительным именем, купленным на Fragment. Если Telegram отвергает разметку сообщений целиком — выключите их переменной CUSTOM_EMOJI=0.

Деплой (Docker + Railway)

Важно про сохранение данных. progress.json — единственный файл, который бот пишет (настройки модели, выбранный стек, прогресс по уровням). Файловая система контейнера на Railway эфемерная: без примонтированного тома этот файл стирается при каждом деплое и при каждом рестарте по расписанию, то есть настройки сбрасываются ежедневно.

Чтобы данные жили:

  1. В сервисе Railway: Attach volume, точка монтирования — например /data/app монтировать нельзя: том перекроет код бота).
  2. Добавить переменную окружения DATA_DIR=/data.

Каталог создаётся автоматически при первом запуске.

docker build -t telegram-bot .

Расписание запуска/остановки настроено в .github/workflows/bot-schedule.yml (09:00–01:00 MSK).

Как это работает

  1. Пользователь пишет вопрос
  2. Бот отправляет сообщение «Думаю...»
  3. В процессе генерации бот обновляет сообщение каждые ~300 символов и обновляет таймер каждые 2 секунды
  4. В финале показывает ответ с HTML-разметкой, счётчиком токенов и заполнением контекста

Размер контекстного окна NVIDIA в API не сообщает (/v1/models отдаёт только id и владельца) и переполнение не отвергает, поэтому величина для индикатора измерена вручную: все пять моделей приняли промпт на 115k токенов, super-120b — на 360k. В конфиге стоят консервативные 128k (PROVIDERS["nvidia"]["context"]) — это ориентир, а не жёсткий предел.

Структура проекта

numbertree_bot/
├── main.py          # Основной код бота
├── research.py      # Веб-поиск (Tavily) и цикл tool calling
├── telegraph.py     # Публикация длинных ответов на telegra.ph
├── tables.py        # Markdown-таблицы в моноширинный вид
├── interview.json   # База вопросов для /interview
├── roadmap.json     # Каталог языков + готовые шаблоны стеков (редактируйте под себя)
├── progress.json    # Настройки стека и прогресс пользователей (создаётся автоматически)
├── requirements.txt # Зависимости
├── .env.example     # Пример конфигурации
└── .env             # Ваши секреты (не коммитить в git!)

Roadmap: свой путь по стеку и языкам

Конфигурация лежит в roadmap.json рядом с ботом и состоит из двух частей:

languages — каталог языков. У каждого языка:

  • id — ключ (например go, python)
  • name — отображаемое имя
  • icon — эмодзи
  • stack — описание стека
  • levels — путь по языку от нуля. Каждый уровень задаёт:
    • title — название уровня
    • difficulty — easy/medium/hard (влияет на генерацию задач)
    • languages — под какой язык генерируются задачи/викторины
    • focus — темы (через запятую), их можно отмечать пройденными в меню
    • tasks — готовые задачи уровня

templates — готовые шаблоны стека. Каждый шаблон — {id, name, icon, description, languages:[id, ...]} — список языков из каталога, чьи уровни идут последовательно. Например «Go · от нуля до мидла» = только путь по Go.

Бот читает roadmap.json при старте. Меняете конфиг — перезапустите бота.

Управление:

  • /stack (или кнопка «🧰 Стек и языки») — выбрать готовый шаблон (✅) или собрать свой стек, поочерёдно отмечая языки (✅). Кнопка «Сбросить» возвращает демо-режим (все языки каталога)
  • /roadmap (или кнопка «🗺️ Roadmap») — уровень показывает активный путь (по выбранному стеку) с прогрессом «темы пройдено/всего»
  • Внутри уровня: отметить тему ✅, сгенерировать задачу по стеку 💻, викторину по языку 🧠, перейти к следующему уровню ➡️
  • /learn — теория + тест по текущему уровню
  • /export (или кнопка «📤 Экспортировать всё») — собирает все уровни + сгенерированные за сессию задачи/викторины в один markdown-файл и отправляет его
  • Выбранный стек и прогресс сохраняются в progress.json и переживают перезапуск
  • Сгенерированный контент хранится в памяти (по чату) и сбрасывается при перезапуске

Техсобеседование: /interview

База вопросов с реальных Go-собеседований лежит в interview.json рядом с ботом (источник — nilchan.com). Структура:

  • categories — категории (горутины, каналы, синхронизация, слайсы/строки, память/GC, PostgreSQL)
  • questions — вопросы. У каждого:
    • category — категория
    • q — формулировка вопроса
    • typemc (тест с вариантами) или open (напиши сам)
    • options + correct — варианты и индекс правильного (для mc)
    • answer — корректный ответ (для open)
    • explanation — разбор «как правильно ответить»

Управление:

  • /interview (или кнопка в меню) — выбрать категорию, случайный вопрос или все вопросы вперемешку
  • Тест (mc) — запускается полл с вариантами; после выбора приходит счёт и объяснение
  • Открытый ответ (open) или просто напишите ответ текстом — LLM оценит ваш вариант по шкале 0-10 и зачтёт его в счёт сессии (балл 7+ = «верно»). Нужен NVIDIA_API_KEY
  • При завершении (🏁 Завершить или слово «завершить/стоп») бот составляет резюме собеседования: итоговый уровень знаний, сильные стороны, что подтянуть и план до middle
  • Счёт сессии и сброс — через кнопку «🏁 Завершить»

About

Telegram-бот на моделях NVIDIA API: стриминг ответов, веб-поиск с проверкой источников и публикацией длинных ответов на Telegraph, roadmap по стеку и техсобеседование по Go

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages