Создание Telegram-бота-репетитора на Python для подготовки к сертификациям. 

В этой статье мы шаг за шагом разработаем Telegram-бота-репетитора на языке Python. Бот будет проводить адаптивные мини-экзамены по темам программирования и анализа данных (Python, Data Science, AI, Machine Learning), хранить результаты в базе данных SQLite и давать пользователю обратную связь о слабых темах и дальнейших шагах в обучении. Мы развернём его в облаке на immers.cloud. Это облачный GPU сервис, предоставляющий доступ к серверам с широким выбором видеокарт для решения самых разнообразных задач: от гейминга и рендеринга 3D-анимаций до Машинного Обучения (МО) и работы с большими языковыми и генеративными моделями.

Почему мы выбрали immers.cloud:

  • Быстрый старт: нужный сервер поднимается за пару минут.
  • Посекундная тарификация — платишь только за время работы сервера
  • Большой ассортимент GPU: 11 моделей NVIDIA RTX и Tesla, включая высокопроизводительные модели, H100 (мой фаворит) и A100 с 80 ГБ видеопамяти с поддержкой GPUDirect и NVLink технологий.
  • Образы с предустановленным окружением для ML задач, чтобы не тратить время на настройку.
  • Поддержка 24/7 и стабильная производительность

Регистрация и настройка Telegram-бота

Прежде всего, зарегистрируем нового бота через официального Telegram-бота BotFather. Для этого в приложении Telegram найдите контакт @BotFather, нажмите Start, и выполните команду /newbot. BotFather попросит указать имя бота (произвольное, отображается в списке чатов) и системное имя пользователя бота (должно быть уникальным и оканчиваться на bot, например, MyTutorBot или Test_Bot). Если все прошло успешно, BotFather зарегистрирует бота и выдаст токен – секретный ключ для доступа к Bot API, вида 123456:ABC-DEF1234ghIkl.... Сохраните этот токен, он понадобится нам в коде для подключения к боту.

Также можно настроить боту описание, аватар и список команд через BotFather (команды /setdescription/setuserpic/setcommands и др.), но на первых порах это необязательно. Главное – получить токен.

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

Установка зависимостей и настройка окружения

Для написания бота нам понадобятся следующие зависимости (Python-библиотеки):

  • pyTelegramBotAPI – неофициальная библиотека для работы с Telegram Bot API (удобна для начинающих, предоставляет класс TeleBot). Установим ее с помощью pip.
  • sqlite3 – модуль стандартной библиотеки Python для работы с SQLite. Его не нужно устанавливать, он входит в Python по умолчаниюhabr.comthecode.media.
  • requests – библиотека для выполнения HTTP-запросов (понадобится для получения вопросов из внешнего API).
  • openai – библиотека OpenAI для вызова API ChatGPT (для генерации вопросов и пояснений).
  • Также может понадобиться python-dotenv (опционально) для удобного чтения токенов из файла .env, или же можно использовать стандартный os.environ.

Установим необходимые библиотеки. В командной строке на вашем компьютере или сервере выполните:

pip install pyTelegramBotAPI requests openai python-dotenv

(Примечание: sqlite3 устанавливать не требуется, это встроенный модуль Python.)

Если вы работаете в облачном окружении immers.cloud (Linux-VM с GPU), вам нужно настроить окружение аналогичным образом. После создания виртуального сервера на immers.cloud и подключения к нему по SSH , убедитесь, что Python установлен (актуальная версия Python 3). Как правило, на Ubuntu уже есть Python 3, но может потребоваться установка менеджера пакетов pip:

sudo apt update && sudo apt install -y python3-pip

Затем установите перечисленные выше библиотеки через pip3 install ....

Если вы планируете высокую нагрузку на бота, immers.cloud позволяет легким движением руки увеличить мощности, в пару кликов, а пока для нашего бота достаточно базового Python-окружения. GPU не обязателен – бот не выполняет тяжелых вычислений, вся логика на CPU.

Настройка ключей: Создайте файл .env (или экспортируйте переменные окружения) с двумя ключами: TELEGRAM_TOKEN (токен вашего Telegram-бота) и OPENAI_API_KEY (ключ OpenAI API для запросов к ChatGPT). Получить OpenAI API Key можно на портале OpenAI в разделе API. Сохраните эти ключи, они понадобятся в коде. На immers.cloud можно задать их в переменных окружения shell (через export в вашем терминале или добавив в ~/.bashrc).

Теперь, когда окружение готово, приступим к созданию файлов проекта.

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

Структура нашего проекта будет следующей:



tutor_bot/
├── bot.py # Основной скрипт Telegram-бота
├── requirements.txt # (опционально) список зависимостей
└── data/
└── questions.db # Файл базы данных SQLite с вопросами и результатами

Мы будем хранить код бота в одном файле bot.py для простоты. В папке data расположен файл базы данных SQLite – назовем его, например, questions.db. Вы можете создать эту папку и файл заранее, либо скрипт сам создаст файл базы при первом запуске.

Совет: Добавьте файл questions.db в .gitignore, если планируете использовать систему контроля версий, чтобы не коммитить базу данных. Также убедитесь, что бот запускается из директории проекта, чтобы относительные пути (например, к БД) работали правильно.

Далее рассмотрим основные компоненты проекта: подключение к базе данных, логика проведения экзамена, интеграция с внешними API.

Работа с базой данных (SQLite)

Мы выбрали SQLite в качестве СУБД за ее простоту и отсутствие необходимости развертывать сервер. Python имеет встроенную поддержку SQLite, поэтому можно сразу использовать sqlite3 без дополнительной установки.

Подготовка базы: В нашей базе данных будет как минимум две таблицы:

  • questions – хранит вопросы викторины.
  • users – хранит информацию о пользователях бота (например, id, имя, username Telegram).
  • results – хранит историю экзаменов (пользователь, дата, результат, слабые темы и т.п.).

Для упрощения создадим эти таблицы программно, если они еще не существуют. Структура таблицы questionsможет быть следующей: id (INTEGER PRIMARY KEY), topic (TEXT), difficulty (TEXT), question (TEXT), option_a/option_b/option_c/option_d (TEXT для вариантов ответов) и correct_option (TEXT, хранит букву правильного варианта: ‘A’, ‘B’, ‘C’ или ‘D’). В таблице users сохраним user_id (INTEGER PRIMARY KEY), first_namelast_nameusername. Таблица results будет содержать id (PRIMARY KEY), user_iddate (TEXT или DATETIME), score (INTEGER), total (INTEGER), и weak_topics (TEXT, например, через запятую перечислены темы, в которых были ошибки).

Соединение с БД: Соединимся с файлом SQLite с помощью sqlite3.connect(). Укажем флаг check_same_thread=False при соединении, чтобы использовать одно соединение из разных потоков бота (библиотека telebot по умолчанию обрабатывает сообщения в многопоточной режиме). Например:

import sqlite3

# Открываем (или создаем) базу данных
conn = sqlite3.connect('data/questions.db', check_same_thread=False)
cursor = conn.cursor()

Путь 'data/questions.db' подразумевает, что существует папка data в проекте. Если её нет, создайте ее заранее или обработайте FileNotFoundError. При первом запуске, если файла нет, будет создан пустой файл базы. Мы должны создать таблицы – проверим, существуют ли они. Это можно сделать, выполнив запрос к служебной таблице sqlite_master. Например, запрос:

sqlКопироватьРедактироватьSELECT count(*) FROM sqlite_master WHERE type='table' AND name='questions';

вернет 0, если таблицы questions нет. В таком случае выполняем команду CREATE TABLE ... для её созданияthecode.media. Аналогично для других таблиц.

После создания структуры базы можно наполнить таблицу questions некоторым набором вопросов. Для демонстрации добавим несколько вопросов вручную (в коде), если таблица пока пуста. В реальном приложении вы бы подготовили более обширную базу вопросов по темам Python/DS/ML.

Пример добавления записи в таблицу:

cursor.execute(
"INSERT INTO questions (topic, difficulty, question, option_a, option_b, option_c, option_d, correct_option) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
("Python", "easy", "Какой тип данных используется для хранения целых чисел в Python?",
"int", "str", "list", "float", "A")
)
conn.commit()

Здесь мы вставили простой вопрос по Python. Коммит зафиксирует изменения. В коде бота мы сделаем подобное для нескольких вопросов разных уровней сложности.

Хранение данных пользователя и результатов: Когда пользователь впервые взаимодействует с ботом (команда /start), можно сохранить его данные в таблицу users (Telegram передает нам message.from_user с полями id, first_name, last_name, username). Мы запишем их, чтобы идентифицировать пользователя в нашей базе. При последующих запусках /start от того же пользователя, можно либо игнорировать повторную вставку (использовать INSERT OR IGNORE, либо сперва проверить существование).

После завершения каждого экзамена бот будет сохранять результат: количество правильных ответов, общее число вопросов и выявленные слабые темы. Эти данные мы внесем в таблицу results с привязкой к пользователю. Имея эту историю, можно в будущем анализировать прогресс пользователя или делать адаптацию уровня сложности на основе прошлых попыток (в нашей реализации мы используем адаптацию в ходе экзамена, но можно и между экзаменами).

Механика экзамена и адаптивная сложность

Основная задача бота – проводить небольшие экзамены-тесты, автоматически подстраивая сложность вопросов под уровень знаний пользователя. Мы реализуем это следующим образом:

  1. Пользователь начинает экзамен (например, командой /exam). Бот создает сессию экзамена для пользователя: обнуляет счетчик правильных ответов, задает начальную сложность вопроса и готовит список (или генератор) вопросов.
  2. Адаптивный алгоритм: Начальный уровень сложности можно выбрать средний (medium) либо опираться на прошлый опыт пользователя. В простейшем случае начнем с easy или medium. После каждого ответа пользователя бот оценивает: если ответ верный – следующий вопрос будет сложнее (повышаем уровень сложности), если неверный – следующий вопрос прощеcanopylab.io. Такой подход реализует принцип adaptive quiz, где сильному студенту даются более сложные задачи, а слабому – более простые, чтобы поддерживать оптимальный уровень сложности и избежать фрустрации или скукиcanopylab.io. В итоге, к концу теста мы получим более точную оценку знаний, чем при фиксированном уровне вопросов.
  3. Выбор вопроса: Для заданной сложности бот выбирает вопрос из доступных источников:
    • Локальная база: у нас есть таблица questions с подготовленными вопросами. В первую очередь бот может брать оттуда (например, для тем, которые точно покрыты и для быстроты).
    • Внешний API: чтобы разнообразить вопросы, можно использовать открытые API. Мы в качестве примера будем пользоваться Open Trivia DB – бесплатным API викторин. У него есть категория “Science: Computers” (ID 18), которая содержит вопросы по компьютерам и программированию. Мы можем запросить у него вопрос нужной сложности, передавая параметр &difficulty=easy|medium|hard. В ответ придет JSON с вопросом, правильным и неправильными ответами.
    • Генерация через ChatGPT: для наиболее сложных или нестандартных вопросов бот может генерировать их на лету через OpenAI API. Например, попросить модель gpt-3.5-turbo сгенерировать вопрос по конкретной теме и уровень сложности, а также предоставить правильный ответ и объяснение.
  4. Задание вопроса пользователю: Бот отправляет пользователю текст вопроса и варианты ответов (если это вопрос с вариантами). Пользователь отвечает, выбрав вариант (например, отправляет ABC или D – мы так договоримся для мульти-выбора). Можно, конечно, позволять ответ отправлять текстом (например, написать слово), но проверка таких ответов сложнее (нужно сравнивать строки, учитывать опечатки и т.п.). Поэтому удобнее ограничиться фиксированными опциями.
  5. Проверка ответа и обратная связь: Бот сравнивает ответ пользователя с правильным. В ответ он отправляет сообщение: либо «✅ Верно!», либо «❌ Неверно, правильный ответ: …». Если у нас есть объяснение (например, сгенерированное ChatGPT), бот тоже отправит краткое пояснение, чтобы ученик понял свой ошибку. Например: «❌ Неверно. Правильный ответ: A (int). Пояснение: В Python целые числа хранятся в типе int, тогда как float – для вещественных чисел.»
  6. Следующий вопрос: После этого бот автоматически задает следующий вопрос, уровень которого уже скорректирован. Так продолжается, пока не будет задано оговоренное число вопросов (например, 5 вопросов за экзамен). Число вопросов можно сделать настройкой – для простоты возьмем фиксировано 5.
  7. Завершение экзамена и рекомендации: Когда вопросы закончились, бот выводит итоги: сколько из N вопросов пользователь ответил правильно, какие темы оказались самыми сложными (т.е. где были ошибки) и что рекомендуется сделать дальше. Например: «Вы правильно ответили на 3 из 5 вопросов. Слабые темы: Python (типы данных), Машинное обучение (регрессия). Рекомендация: подтяните эти темы – изучите материалы по основам Python, затем переходите к разделу по регрессии, прежде чем пытаться решать более сложные задачи.»

Обратная связь должна быть мотивирующей и конкретной. Бот может предлагать конкретные темы для проработки. Мы можем заложить простую логику: выводить топ-1-2 темы, где были ошибки, и советовать пройтись по ним прежде чем двигаться дальше. Например, если выявлено, что пользователь не знает основ Python, совет будет начать с них, потом переходить к Data Science. Если слабое место – Machine Learning, можно посоветовать изучить основы ML, а затем уже углубляться в AI. То есть, выстраиваем упрощенный трек подготовки: от текущего пробела – к следующему этапу.

В коде мы для рекомендаций используем заранее заданные зависимости между темами. Например, определим условно, что порядок освоения: Python -> Data Science -> Machine Learning -> AI. Тогда, если слабая тема Python, совет: “начните с основ Python, затем переходите к библиотекам для Data Science”. Если слабая тема Machine Learning, совет: “разберите фундаментальные алгоритмы ML, а потом можно двигаться к разделу AI (нейросети, глубокое обучение)”. Конечно, можно сделать и тоньше (учитывать конкретные подтемы), но останемся при простом варианте.

Работа с внешними API и ChatGPT

Для получения разнообразных вопросов мы интегрируем два внешних источника:

  1. Open Trivia DB (внешний API вопросов): Это открытая база вопросов с REST API. Нам повезло – у неё есть категория Computers, которая включает вопросы по ИТ. Запрос выглядит так: https://opentdb.com/api.php?amount=1&category=18&difficulty=easy&type=multiple. Параметры:
    • amount=1 – сколько вопросов получить (нам нужен один за раз).
    • category=18 – ID категории “Science: Computers”.
    • difficulty=easy|medium|hard – уровень сложности.
    • type=multiple – тип вопроса: с несколькими вариантами (так мы получим варианты и один правильный ответ).
    Библиотекой requests делаем GET-запрос к этому URL и получаем JSON. Пример ответа (сокращенно):jsonКопироватьРедактировать{ "response_code": 0, "results": [ { "category": "Science: Computers", "type": "multiple", "difficulty": "easy", "question": "What does CPU stand for?", "correct_answer": "Central Processing Unit", "incorrect_answers": ["Central Process Unit", "Computer Personal Unit", "Central Processor Unit"] } ] } Из этого берём questioncorrect_answer и формируем список вариантов (правильный + неправильные). Порядок можно перемешать, чтобы правильный не всегда был, скажем, первым. Мы также должны сохранить, какой вариант правильный, чтобы потом проверить ответ пользователя.Отметим, что Open Trivia DB возвращает тексты вопросов и ответов с возможными HTML-сущностями (например, " вместо кавычек). Стоит либо их декодировать, либо использовать как есть (библиотека html из Python может помочь: html.unescape(string)).
  2. OpenAI ChatGPT API: Используя модель GPT-3.5 или GPT-4, мы можем на лету генерировать новые вопросы. Это особенно полезно для сложных вопросов или когда своя база вопросов ограничена. Через библиотеку openai мы сделаем запрос типа chat completion. Например, сформируем системное сообщение, что модель – помощник-репетитор, и пользовательское сообщение примерно такое: “Сгенерируй сложный вопрос по теме нейронных сетей (Machine Learning) с четырьмя вариантами ответа (A, B, C, D), укажи правильный вариант и короткое объяснение ответа.”.Модель в ответ могла бы вернуть текст, в котором будет:
    • Вопрос.
    • Варианты A, B, C, D.
    • Правильный ответ (например, «Правильный ответ: C») и объяснение.
    Чтобы удобно обработать ответ от модели, можно попросить её вывести в формате, удобном для парсинга (например, JSON или четко размеченный текст). Однако, для простоты мы можем принять от нее просто блочный текст и постараться его разобрать регулярными выражениями или разделителями. В рамках нашей статьи мы ограничимся тем, чтобы показать интеграцию и основы работы с API.Внимание: Использование OpenAI API – платное (по количеству токенов). Для экспериментов OpenAI дает небольшие кредиты, но убедитесь, что понимаете тарифы. В нашем коде мы постараемся минимизировать токены (например, запрашивать только необходимый текст).

Настройка OpenAI API в коде: Перед отправкой запросов нужно указать API-ключ. Мы можем либо напрямую прописать openai.api_key = "ваш-ключ" (что нежелательно хранить в коде), либо прочитать его из окружения:

import openai
import os

openai.api_key = os.getenv("OPENAI_API_KEY")

Если ключ не задан, функции генерации вопросов могут быть пропущены или вызвать исключение – в нашем коде мы это предусмотрим.

Теперь, когда общая концепция ясна, представим конечный код бота, объединяющий все обсуждённые компоненты.

Полный код Telegram-бота (Python)

Ниже приведён полный исходный код bot.py. Он включает подключение к БД, реализацию команд бота, получение вопросов из всех источников и логику адаптивного тестирования. Комментарии в коде поясняют работу каждого блока:

import os
import sqlite3
import requests
import random
import html
import openai
import telebot
from datetime import datetime

# Инициализация Telegram-бота
API_TOKEN = os.getenv("TELEGRAM_TOKEN") # токен бота, можно прописать строкой, например: "123456:ABC-..."
if API_TOKEN is None:
API_TOKEN = "<YOUR_TELEGRAM_BOT_TOKEN>" # TODO: вставьте свой токен сюда, если не используете переменные окружения

bot = telebot.TeleBot(API_TOKEN)

# Инициализация OpenAI API
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if OPENAI_API_KEY:
openai.api_key = OPENAI_API_KEY

# Подключение к базе данных SQLite
os.makedirs("data", exist_ok=True) # создаем папку для базы, если её нет
conn = sqlite3.connect("data/questions.db", check_same_thread=False)
cursor = conn.cursor()

# Функция для проверки существования таблицы и её создания при необходимости
def ensure_tables():
# Таблица вопросов
cursor.execute("CREATE TABLE IF NOT EXISTS questions ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"topic TEXT,"
"difficulty TEXT,"
"question TEXT,"
"option_a TEXT,"
"option_b TEXT,"
"option_c TEXT,"
"option_d TEXT,"
"correct_option TEXT)")
# Таблица пользователей
cursor.execute("CREATE TABLE IF NOT EXISTS users ("
"user_id INTEGER PRIMARY KEY,"
"first_name TEXT,"
"last_name TEXT,"
"username TEXT)")
# Таблица результатов экзаменов
cursor.execute("CREATE TABLE IF NOT EXISTS results ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"user_id INTEGER,"
"date TEXT,"
"score INTEGER,"
"total INTEGER,"
"weak_topics TEXT)")
conn.commit()

# Если таблица questions пуста, добавим несколько вопросов для примера
cursor.execute("SELECT COUNT(*) FROM questions")
count = cursor.fetchone()[0] if count == 0:
# Примеры вопросов (для демонстрации)
sample_questions = [
# topic, difficulty, question, A, B, C, D, correct_option
("Python", "easy", "Какой тип данных используется для хранения целых чисел в Python?",
"int", "str", "list", "float", "A"),
("Python", "medium", "Что выведет код: print(len([1, 2, 3])) ?",
"3", "2", "Ошибка", "None", "A"),
("Data Science", "easy", "Какой библиотекой Python удобно пользоваться для анализа данных (таблицы и DataFrame)?",
"Pandas", "NumPy", "Matplotlib", "TensorFlow", "A"),
("Machine Learning", "medium", "Как называется задача обучения с учителем для предсказания категориальной переменной?",
"Классификация", "Регрессия", "Кластеризация", "Редукция измерений", "A"),
("AI", "hard", "Какой механизм обучения использует метод обратного распространения ошибки в нейронных сетях?",
"Градиентный спуск", "Эволюционные алгоритмы", "Поиск в глубину", "Байесовский вывод", "A")
] cursor.executemany(
"INSERT INTO questions (topic, difficulty, question, option_a, option_b, option_c, option_d, correct_option) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
sample_questions
)
conn.commit()

# Убедимся, что нужные таблицы есть
ensure_tables()

# Словарь для хранения состояния экзамена пользователей
user_exam_state = {}

# Структура user_exam_state[user_id] = {
# "in_progress": bool,
# "score": int,
# "total": int,
# "current_q": dict (question, options, correct_option, topic, difficulty, explanation),
# "difficulty": str (current difficulty level),
# "asked": int (how many questions asked so far),
# "wrong_topics": set() (topics user answered wrong in this exam)
# }

# Помощная функция: выбрать следующий уровень сложности (адаптивно)
def next_difficulty(current_diff, answer_was_correct):
levels = ["easy", "medium", "hard"] idx = levels.index(current_diff) if current_diff in levels else 1 # default to medium if unknown
if answer_was_correct and idx < len(levels) - 1:
# повысить сложность
return levels[idx + 1] elif not answer_was_correct and idx > 0:
# понизить сложность
return levels[idx - 1] else:
# остаётся той же, либо достигнут крайний уровень
return current_diff

# Функция для выбора вопроса из локальной базы по заданной сложности
def get_question_from_db(difficulty):
cursor.execute("SELECT id, topic, question, option_a, option_b, option_c, option_d, correct_option "
"FROM questions WHERE difficulty = ? ORDER BY RANDOM() LIMIT 1", (difficulty,))
row = cursor.fetchone()
if row:
q_id, topic, question_text, a, b, c, d, correct_opt = row
options = {"A": a, "B": b, "C": c, "D": d}
return {
"source": "db",
"topic": topic,
"difficulty": difficulty,
"question": question_text,
"options": options,
"correct": correct_opt,
"explanation": None # пояснение можно отдельно хранить или генерировать
}
return None

# Функция получения вопроса из Open Trivia DB API
def get_question_from_api(difficulty):
try:
url = f"https://opentdb.com/api.php?amount=1&category=18&difficulty={difficulty}&type=multiple"
res = requests.get(url, timeout=5)
data = res.json()
if data.get("response_code") == 0:
result = data["results"][0] question_text = html.unescape(result["question"])
correct_answer = html.unescape(result["correct_answer"])
incorrect_answers = [html.unescape(ans) for ans in result["incorrect_answers"]] # Формируем варианты (случайно перемешаем позиции)
options_list = incorrect_answers + [correct_answer] random.shuffle(options_list)
options = {}
correct_opt = None
option_labels = ["A", "B", "C", "D"] for idx, opt_text in enumerate(options_list):
label = option_labels[idx] options[label] = opt_text
if opt_text == correct_answer:
correct_opt = label
return {
"source": "api",
"topic": "Computers",
"difficulty": difficulty,
"question": question_text,
"options": options,
"correct": correct_opt,
"explanation": None
}
except Exception as e:
print(f"Ошибка при запросе к Open Trivia API: {e}")
return None

# Функция генерации вопроса через OpenAI (ChatGPT)
def generate_question_via_gpt(topic, difficulty):
if not OPENAI_API_KEY:
return None # нет ключа API, не можем генерировать
try:
prompt = (f"Сгенерируй один вопрос викторины на русском языке по теме {topic} уровня сложности {difficulty}. "
f"Дай четыре варианта ответа (A, B, C, D), укажи правильный вариант и кратко объясни правильный ответ.")
# Отправляем запрос модели
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "Ты – помощник, генерирующий вопросы для экзамена."},
{"role": "user", "content": prompt}
],
temperature=0.7
)
text = response.choices[0].message.content.strip()
# Простейший парсинг результата:
# Предположим, модель вернула текст формата:
# "Вопрос: ...?\nA. ...\nB. ...\nC. ...\nD. ...\nПравильный ответ: ...\nПояснение: ..."
lines = text.splitlines()
question_text = ""
options = {}
correct_opt = None
explanation = ""
for line in lines:
line = line.strip()
if line.lower().startswith("вопрос"):
# Например: "Вопрос: Какой...?"
parts = line.split(":", 1)
if len(parts) > 1:
question_text = parts[1].strip()
elif re.match(r"^[A-D]\.? ", line): # строки начинающиеся с "A. ", "B. " и т.д.
# Например: "A. вариант ответа"
label = line[0] opt_text = line[3:].strip() # отсекаем "A. "
options[label] = opt_text
elif line.lower().startswith("правильный ответ"):
# Например: "Правильный ответ: C"
parts = line.split(":")
if len(parts) > 1:
correct_opt = parts[1].strip().upper()
# correct_opt будет "C" например
elif line.lower().startswith("пояснение"):
# например: "Пояснение: ... "
parts = line.split(":", 1)
if len(parts) > 1:
explanation = parts[1].strip()
else:
# На случай если пояснение или вопрос продолжаются на нескольких строках
if correct_opt is None:
# еще часть вопроса
if question_text:
question_text += " " + line
else:
# после отметки правильного ответа считаем, что остальное - пояснение
explanation += " " + line

if not question_text or not options or not correct_opt:
# если парсинг не удался, вернем None
return None
# Убедимся, что варианты помечены буквами A-D:
# (вдруг модель дала не в том формате)
for lbl in ["A", "B", "C", "D"]:
if lbl not in options:
return None # формат неожиданный, пропустим
return {
"source": "gpt",
"topic": topic,
"difficulty": difficulty,
"question": question_text,
"options": options,
"correct": correct_opt,
"explanation": explanation
}
except Exception as e:
print(f"Ошибка при запросе к OpenAI: {e}")
return None

# Хэндлер команды /start
@bot.message_handler(commands=['start'])
def send_welcome(message):
user = message.from_user
# Сохраняем пользователя в БД (если еще нет)
try:
cursor.execute("INSERT OR IGNORE INTO users (user_id, first_name, last_name, username) VALUES (?, ?, ?, ?)",
(user.id, user.first_name, user.last_name, user.username))
conn.commit()
except Exception as e:
print(f"DB error on inserting user: {e}")
# Приветственное сообщение
bot.reply_to(message,
f"Привет, *{user.first_name}*! 🤖\n"
"Я бот-репетитор. Помогу проверить твои знания по Python, Data Science, Machine Learning и AI.\n"
"Чтобы начать тест, отправь команду /exam.\n"
"Для отмены теста – /cancel.\n"
"Удачи!",
parse_mode='Markdown')

# Хэндлер команды /exam - начать экзамен
@bot.message_handler(commands=['exam'])
def start_exam(message):
user_id = message.from_user.id
# Инициализируем состояние экзамена для пользователя
user_exam_state[user_id] = {
"in_progress": True,
"score": 0,
"total": 5, # планируем 5 вопросов
"asked": 0,
"difficulty": "easy", # начнем с лёгкого, можно изменить на "medium"
"current_q": None,
"wrong_topics": set()
}
# Отправляем первое задание
send_next_question(message.chat.id, user_id)

# Функция отправки следующего вопроса пользователю
def send_next_question(chat_id, user_id):
state = user_exam_state.get(user_id)
if not state or not state["in_progress"]:
return # если тест не запущен, ничего не делаем
# Если уже задали достаточно вопросов, заканчиваем тест
if state["asked"] >= state["total"]:
finish_exam(chat_id, user_id)
return
difficulty = state["difficulty"] q_data = None

# Попробуем циклически брать вопросы из разных источников для разнообразия
# Например: 1-й вопрос из БД, 2-й из API, 3-й из GPT, затем повторяем.
order = ["db", "api", "gpt"] source = order[state["asked"] % len(order)] if source == "db":
q_data = get_question_from_db(difficulty)
# если из базы не нашлось (например, нет такого уровня), fallback на API
if not q_data:
q_data = get_question_from_api(difficulty)
elif source == "api":
q_data = get_question_from_api(difficulty)
if not q_data:
q_data = get_question_from_db(difficulty)
elif source == "gpt":
# Для GPT выберем случайную тему из списка основных тем, соответствующую текущей сложности
topics_for_diff = {"easy": "Python", "medium": "Machine Learning", "hard": "AI"}
topic = topics_for_diff.get(difficulty, "Python")
q_data = generate_question_via_gpt(topic, difficulty)
if not q_data:
# если не удалось сгенерировать, подстрахуемся вопросом из базы
q_data = get_question_from_db(difficulty) or get_question_from_api(difficulty)
else:
q_data = get_question_from_db(difficulty)

if not q_data:
# Не удалось получить вопрос (что мало вероятно при наличии базы и API)
bot.send_message(chat_id, "Не удалось получить следующий вопрос. Завершаю тест.")
finish_exam(chat_id, user_id)
return

# Сохраняем текущий вопрос в состоянии пользователя
state["current_q"] = q_data
state["asked"] += 1
user_exam_state[user_id] = state

# Формируем текст вопроса для отправки
q_text = f"*Вопрос {state['asked']}/{state['total']}* ({q_data['difficulty']}):\n{q_data['question']}\n\n"
# Добавим варианты ответа
for label, option_text in q_data["options"].items():
q_text += f"{label}. {option_text}\n"
q_text += "\n*Ваш ответ?* (отправьте букву A, B, C или D)"
bot.send_message(chat_id, q_text, parse_mode='Markdown')

# Хэндлер команды /cancel - досрочное прерывание экзамена
@bot.message_handler(commands=['cancel'])
def cancel_exam(message):
user_id = message.from_user.id
state = user_exam_state.get(user_id)
if state and state.get("in_progress"):
# Сбросить состояние
user_exam_state[user_id]["in_progress"] = False
bot.reply_to(message, "Экзамен прерван. Если захотите начать заново, отправьте /exam.")
else:
bot.reply_to(message, "Нет активного экзамена. Чтобы начать новый, отправьте /exam.")

# Хэндлер текстовых сообщений (предполагаем, что это ответы на вопросы во время экзамена)
@bot.message_handler(func=lambda msg: True, content_types=['text'])
def handle_answer(message):
user_id = message.from_user.id
state = user_exam_state.get(user_id)
# Если пользователь не в режиме экзамена, можно отвечать что не понимаем команду или игнорировать
if not state or not state.get("in_progress"):
# Неактивный режим - можно, например, вывести подсказку
bot.reply_to(message, "Я вас не понял. Чтобы начать тест, введите /exam.")
return

# Обрабатываем ответ пользователя
answer = message.text.strip()
if not answer:
return
answer = answer[0].upper() # берем первую букву, переводим в верхний регистр (на случай, вдруг напишет слово)
current_q = state.get("current_q")
if not current_q:
return # на всякий случай, не задан вопрос

correct_option = current_q["correct"] # Проверим ответ
if answer == correct_option:
state["score"] += 1
response_text = "✅ *Верно!*"
else:
# Запомнить тему неправильного ответа
state["wrong_topics"].add(current_q["topic"])
correct_answer_text = current_q["options"].get(correct_option, "")
response_text = f"❌ *Неверно.* Правильный ответ: {correct_option}."
if correct_answer_text:
response_text += f" ({correct_answer_text})"
# Если есть пояснение и пользователь ошибся (или даже если верно, можно дать дополнительную инфо)
if current_q.get("explanation"):
response_text += "\n*Пояснение:* " + current_q["explanation"] bot.reply_to(message, response_text, parse_mode='Markdown')

# Определяем следующую сложность
new_difficulty = next_difficulty(state["difficulty"], answer == correct_option)
state["difficulty"] = new_difficulty
# Обновляем состояние
user_exam_state[user_id] = state

# Задержка перед следующим вопросом может улучшить восприятие (например, 1 сек), но не обязательно
# time.sleep(1)

# Отправляем следующий вопрос
send_next_question(message.chat.id, user_id)

# Функция завершения экзамена
def finish_exam(chat_id, user_id):
state = user_exam_state.get(user_id)
if not state:
return
# Отправляем результаты
score = state["score"] total = state["total"] # Определяем слабые темы из текущего теста
weak_topics = state["wrong_topics"] if weak_topics:
weak_list = ", ".join(weak_topics)
feedback = f"Слабые темы: {weak_list}."
else:
feedback = "Вы отлично справились со всеми вопросами!"
result_text = (f"🏁 Экзамен завершён!\n"
f"Ваш результат: *{score}* из *{total}*.\n"
f"{feedback}\n")
# Добавим совет по обучению:
if weak_topics:
# Определим порядок рекомендаций (простая иерархия тем)
priority = {"Python": 1, "Data Science": 2, "Machine Learning": 3, "AI": 4}
# Отсортируем слабые темы по этой приоритетности
sorted_topics = sorted(list(weak_topics), key=lambda t: priority.get(t, 5))
if sorted_topics:
first_topic = sorted_topics[0] # Определим, что предложить дальше. Возьмем следующую тему по сложности, если есть
next_topic = None
if len(sorted_topics) > 1:
# если несколько слабых тем, возьмем вторую по приоритету как next
next_topic = sorted_topics[1] else:
# если одна слабая, предложим следующую тему из иерархии
current_prio = priority.get(first_topic, 5)
for topic, pr in priority.items():
if pr == current_prio + 1:
next_topic = topic
break
if next_topic:
result_text += f"Рекомендуем подтянуть тему *{first_topic}*, затем перейти к *{next_topic}*."
else:
result_text += f"Рекомендуем уделить время теме *{first_topic}*, чтобы устранить пробелы."
else:
result_text += "Продолжайте в том же духе и переходите к более сложным темам! 💪"

bot.send_message(chat_id, result_text, parse_mode='Markdown')

# Сохранение результата в базе данных
try:
date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
weak_topics_str = ", ".join(weak_topics) if weak_topics else ""
cursor.execute("INSERT INTO results (user_id, date, score, total, weak_topics) VALUES (?, ?, ?, ?, ?)",
(user_id, date_str, score, total, weak_topics_str))
conn.commit()
except Exception as e:
print(f"DB error on inserting result: {e}")

# Очистить состояние
user_exam_state[user_id]["in_progress"] = False

# Запуск бота (непрерывный опрос обновлений)
if __name__ == "__main__":
print("Bot is polling...")
bot.polling(none_stop=True)

Этот скрипт делает следующее:

  • Подключается к базе данных и создает таблицы (ensure_tables()), добавляет примеры вопросов при первом запуске.
  • Реагирует на команду /start: регистрирует пользователя в базе, отправляет приветствие и инструкцию.
  • Реагирует на /exam: инициализирует состояние экзамена (5 вопросов, стартовая сложность easy), затем отправляет первый вопрос через функцию send_next_question.
  • Реагирует на /cancel: позволяет прервать экзамен досрочно, очищая состояние.
  • Обрабатывает текстовые сообщения (ответы пользователя) в handle_answer: проверяет ответ, отправляет отзыв (верно/неверно + пояснение), обновляет счет и уровень сложности через next_difficulty (адаптивная логика), и вызывает следующий вопрос или завершает экзамен.
  • По завершении (finish_exam) – считает результат, выявляет слабые темы и формирует рекомендацию. Результат сохраняется в таблицу results.
  • Источники вопросов: send_next_question циклически чередует три источника – локальная БД, API, GPT – для каждого последующего вопроса. Если какой-то источник не выдал вопрос, предусмотрен запасной вариант (например, если GPT не ответил, берем вопрос из базы или API).
  • В коде присутствуют отладочные print при исключениях, чтобы было проще диагностировать проблемы при запуске на сервере.
  • Запуск bot.polling(none_stop=True) в блоке if __name__ == "__main__": запускает бесконечный цикл опроса Telegram-сервера на новые сообщения. Параметр none_stop=True важен, чтобы бот не прекращал работу в случае кратковременного сбоя соединения.

Обратите внимание: код использует простую проверку ответов (по первой букве). Пользователю нужно отправлять именно букву варианта. В реальности, можно улучшить, например, принять цифры 1-4 или не обращать внимания на регистр/лишние пробелы.

Также, адаптивная логика next_difficulty повышает или понижает сложность на один уровень за каждый вопросcanopylab.io. Это один из простых подходов к Computerized Adaptive Testing. Более продвинутые алгоритмы могут учитывать статистику ответов, сложности вопросов по шкале и др. Но даже такой простой метод уже делает тест динамическим и более персонализированным.

Пример работы

Рассмотрим, как будет выглядеть взаимодействие с нашим ботом со стороны пользователя:

  • Шаг 1: Пользователь находит бота в Telegram (по username, выданному BotFather, например: @MyTutorBot) и нажимает «Start» или вводит команду /start.
  • Бот: Приветствует пользователя по имени и даёт инструкцию. Например:Бот: «Привет, Иван! 🤖
    Я бот-репетитор… Чтобы начать тест, отправь команду /exam…»
  • Шаг 2: Пользователь отправляет команду /exam для начала экзамена.
  • Бот: Отправляет первый вопрос. Допустим, начальный уровень – easy, вопрос взят из базы.Бот: «Вопрос 1/5 (easy):
    Какой тип данных используется для хранения целых чисел в Python?
    A. int
    B. str
    C. list
    D. floatВаш ответ? (отправьте букву A, B, C или D)»
  • Шаг 3: Пользователь отвечает, например, отправляет сообщение с текстом A.
  • Бот: Проверяет – вариант A соответствует int, это правильный ответ. Бот отвечает:Бот: «✅ Верно!»(Пояснение не предоставлено для верного ответа в нашем случае, хотя можно было бы добавить дополнительный факт.)Затем бот повышает сложность вопроса до medium и через секунду присылает следующий вопрос.
  • Бот: «Вопрос 2/5 (medium):
    Что выведет код: print(len([1, 2, 3])) ?
    A. 3
    B. 2
    C. Ошибка
    D. NoneВаш ответ?»
  • Шаг 4: Пользователь отвечает, скажем, B (ошибается, т.к. длина списка из 3 элементов равна 3).
  • Бот: Проверяет – вариант B неправильный (правильный A). Бот отвечает:Бот: «❌ Неверно. Правильный ответ: A. (3)»Бот фиксирует тему вопроса («Python») как слабую, понижает сложность до easy (adaptive: раз ошибся на medium, спускаемся). Потом бот задает следующий вопрос (в порядке чередования источников, третий вопрос будет из GPT, предположим сложность easy и тема выбрана “Python” для GPT-сгенерированного вопроса).
  • Бот: «Вопрос 3/5 (easy): …» (GPT-сгенерированный вопрос, например по Python, с вариантами).
  • … Пользователь отвечает на 3-й, 4-й, 5-й вопрос. Допустим, он где-то еще ошибся, а что-то ответил верно.
  • Шаг 5: После ответа на 5-й вопрос бот отправляет заключительное сообщение с результатами:Бот: «🏁 Экзамен завершён!
    Ваш результат: 3 из 5.
    Слабые темы: Python, Machine Learning.
    Рекомендуем подтянуть тему Python, затем перейти к Machine Learning.»Здесь бот перечислил темы, где были ошибки, и дал рекомендацию: сначала разобраться с Python (базой), потом браться за ML (поскольку без уверенного Python сложно в ML). Если бы ошибок не было, бот похвалил бы и предложил двигаться к более сложным темам.
  • Шаг 6: Пользователь может по желанию снова запустить /exam для нового теста и отслеживать свой прогресс. История результатов сохранена в базе, и в дальнейшем можно, например, реализовать команду /stats для показа статистики (не вошло в текущий код, но идея понятна).

Таким образом, бот обеспечивает цикл: приветствие → экзамен (вопросы/ответы) → результаты/советы.

Развёртывание бота на сервере (immers.cloud)

Разработав и отладив бота локально, вы можете развернуть его для постоянной работы на сервере, чтобы он был онлайн 24/7. Шаги для деплоя:

  1. Создание виртуального сервера: Войдите в панель immers.cloud, создайте новую виртуальную машину. Выберите ОС, например Ubuntu 20.04/22.04 (подойдет и другой Linux). Для нашего бота не требуется мощный GPU, но вы можете выбрать конфигурацию по потребностям. Если планируете в перспективе использовать GPU (например, для ML моделей), выберите образ с поддержкой CUDA. Иначе – достаточно стандартного Linux-образа.
  2. Подключение по SSH: После создания ВМ получите её IP-адрес. Подключитесь с помощью SSH-клиента (в Linux/Mac командой ssh, в Windows – через PuTTY или Windows Terminal). Логин по умолчанию может быть ubuntu или root (смотрите в документации immers.cloud, обычно указывается при создании). Вам также понадобятся ключ SSH или пароль – настройте их при создании машины.
  3. Настройка окружения на сервере: Выполните обновление пакетов и установку Python (если необходимо). Часто Ubuntu уже имеет python3. Проверьте: python3 --version. Установите pip и нужные библиотеки:bashКопироватьРедактироватьsudo apt update sudo apt install -y python3-pip pip3 install pyTelegramBotAPI requests openai (Также установите python-dotenv, если планируете его использовать.)
  4. Развёртывание кода: Перенесите файлы проекта на сервер. Это можно сделать несколькими способами:
    • Клонировать репозиторий с GitHub (если код у вас в репозитории).
    • Воспользоваться scp/rsync для копирования файлов. Например, из локальной машины: scp -r tutor_bot/ ubuntu@<IP-адрес>:/home/ubuntu/.
    • Либо вручную создать bot.py на сервере (скопировать содержимое через текстовый редактор nano/vim).
  5. Настройка токенов на сервере: Самый простой путь – задать переменные окружения перед запуском. Можно экспортировать их в консоли:bashКопироватьРедактироватьexport TELEGRAM_TOKEN="<ваш токен от BotFather>" export OPENAI_API_KEY="<ваш ключ OpenAI>" Эти команды нужно выполнить в сессии перед запуском бота. Чтобы не вводить каждый раз, можно добавить их в ~/.bashrc пользователя (тогда они будут автоматически задаваться при входе).
  6. Запуск бота: Запустите скрипт:bashКопироватьРедактироватьpython3 bot.py Если все настроено правильно, вы увидите в консоли сообщение Bot is polling... и бот начнет подключаться к Telegram. Попробуйте отправить боту команду /start из приложения Telegram – убедитесь, что он отвечает.
  7. Фоновый режим: Закрытие SSH-сессии обычно остановит запущенный процесс. Поэтому для постоянной работы бота сделайте одно из:
    • Запустить бота внутри screen или tmux (утилиты для терминала). Например, выполните screen -S mybot, внутри появившейся сессии запустите python3 bot.py. Затем нажмите Ctrl+A, потом D – это отсоединит сессию, оставив бота работать в фоне. Позже можно вернуться командой screen -r mybot.
    • Либо использовать nohup для запуска без привязки к терминалу: nohup python3 bot.py &. Это запустит бота в фоне, вывод логов будет в файл nohup.out.
    • Более продвинутый способ – настроить сервис systemd. Создайте unit-файл, как описывалось в ряде гайдовstackoverflow.comstackoverflow.com, чтобы бот запускался как служба и автоматически рестартовал при сбоях или перезагрузке сервера. Этот способ надежнее для долгосрочного деплоя.
  8. Проверка и сопровождение: Убедитесь, что бот действительно онлайн и отвечает. Можно использовать команду systemctl status yourbot (если настроили systemd) или просто периодически проверять в Telegram. Логи работы можно посмотреть либо через journalctl (для systemd), либо открыв файл nohup.out или reattach-нув screen-сессию.

Теперь ваш бот-репетитор постоянно находится в сети. Вы можете совершенствовать его: добавить больше вопросов в базу, расширить логику рекомендаций, реализовать команду просмотра прогресса, интегрировать другие источники вопросов и т.д.

Желаем успехов в обучении и развитии вашего Telegram-бота! Пусть он поможет многим новичкам прокачаться в Python и Data Science 🚀.

+1
0
+1
7
+1
0
+1
0
+1
1

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *