Родилась у меня идея! Я хочу создать фреймворк, который позволит пользователям писать своих ботов Telegram с помощью языка, специфичного для конкретной области (DSL), или визуального представления, например, диаграммы UML. На основе предоставленных данных фреймворк будет генерировать необходимый Python-код для создания полнофункционального Telegram-бота. Которого можно будет сразу запустить где то на хостинге.

Ну что же. Начинать надо с плана.


  1. Доменно-специфический язык (DSL) или визуальное представление: Нужно разработать DSL или визуальное представление (например, UML-диаграммы), которое позволит пользователям описывать структуру меню, логику и переходы бота. Мы можем использовать комбинацию JSON, YAML или XML для определения DSL или создать простой графический интерфейс для проектирования UML-диаграмм.

  2. Парсер: Разработать парсер, который может читать и интерпретировать DSL или визуальное представление. Парсер должен уметь понимать структуру меню, логику и переходы, определенные пользователем, и переводить их во внутреннее представление, которое может быть использовано для генерации кода.

  3. Генерация кода: Реализовать модуль генерации кода, который принимает внутреннее представление, созданное парсером, и генерирует код Python, используя библиотеку aiogram.

  4. Развертывание и интеграция: Предоставить возможность развернуть сгенерированный код бота на хостинговой платформе и интегрировать его с API Telegram, чтобы бот стал функциональным. Кроме того, создать простой процесс обновления бота при внесении изменений в DSL или визуальное представление.

  5. Документация и учебники: Разработать полную документацию и учебники, чтобы помочь пользователям понять, как использовать фреймворк и создавать собственных ботов Telegram с помощью предоставленного DSL или визуального представления.

4 и 5 пункт пока еще в стадии проработки. Но первые три мы за сегодня сделаем.

  1. Я решил использовать YAML для DSL, так как он является человекочитаемым и простым для понимания. Вот простой пример структуры DSL для описания меню, логики и переходов бота Telegram:

bot:
  token: YOUR_TELEGRAM_BOT_TOKEN
  start_message: Welcome to the sample bot!

states:
  START:
    actions:
      - type: send_message
        text: "Choose an option:"
      - type: show_keyboard
        options:
          - text: Show random number
            target_state: RANDOM_NUMBER
          - text: Tell a joke
            target_state: JOKE

  RANDOM_NUMBER:
    actions:
      - type: send_random_number
        min: 1
        max: 100
        text: "Here's a random number between 1 and 100: {}"
      - type: back_to_start
        text: Back to main menu

  JOKE:
    actions:
      - type: send_joke
        text: "Here's a funny joke: {}"
      - type: back_to_start
        text: Back to main menu

В этом примере файл YAML структурирован на различные секции:

  1. bot: Содержит специфическую для бота информацию, такую как токен и стартовое сообщение.

  2. states: Представляет различные состояния или пункты меню бота. Каждое состояние имеет уникальный идентификатор (например, START, RANDOM_NUMBER, JOKE) и содержит список действий.

  3. actions: Определяет действия, которые должны быть выполнены, когда пользователь взаимодействует с ботом в определенном состоянии. Действия включают отправку сообщения, показ клавиатуры, отправку случайного числа и многое другое.

Поле type в разделе actions определяет тип действия, которое должно быть выполнено, например, отправка сообщения или показ клавиатуры. Поле target_state используется для определения состояния, в которое нужно перейти, когда пользователь выберет определенную опцию.

Это простой DSL, который может быть расширен для включения более сложной логики и дополнительных действий. Со временем можно будет прикрутить какой нибудь плагин к Miro или Figma. Следующим шагом будет создание парсера, который может читать этот DSL и генерировать необходимый Python-код, используя библиотеку aiogram.

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

import yaml


class BotDefinition:
    def __init__(self, yaml_file):
        with open(yaml_file, "r") as f:
            self.data = yaml.safe_load(f)

    def get_bot_token(self):
        return self.data["bot"]["token"]

    def get_start_message(self):
        return self.data["bot"]["start_message"]

    def get_states(self):
        return self.data["states"]


def parse_bot_definition(yaml_file):
    return BotDefinition(yaml_file)


if __name__ == "__main__":
    bot_definition = parse_bot_definition("example.yaml")
    print(bot_definition.get_bot_token())
    print(bot_definition.get_start_message())
    print(bot_definition.get_states())

В данной реализации класс BotDefinition отвечает за загрузку и обработку YAML-файла. Функция parse_bot_definition принимает в качестве аргумента путь к YAML файлу и возвращает объект BotDefinition.

Объект BotDefinition предоставляет методы для доступа к специфической для бота информации (например, get_bot_token и get_start_message), а также к состояниям и действиям, определенным в DSL (например, get_states).

Вы можете проверить эту реализацию с помощью ранее предоставленного примера YAML DSL, сохранив его в файл с именем "example.yaml" и запустив скрипт парсера.

После того, как парсер будет работать правильно, следующим шагом будет создание модуля генерации кода, который принимает объект BotDefinition и генерирует код Python, используя библиотеку aiogram. Этот код будет определять структуру меню, логику и переходы для бота Telegram, как указано в YAML DSL.

YOUR\_TELEGRAM\_BOT\_TOKEN Welcome to the sample bot! {'START': {'actions': \[{'type': 'send\_message', 'text': 'Choose an option:'}, {'type': 'show\_keyboard', 'options': \[{'text': 'Show random number', 'target\_state': 'RANDOM\_NUMBER'}, {'text': 'Tell a joke', 'target\_state': 'JOKE'}\]}\]}, 'RANDOM\_NUMBER': {'actions': \[{'type': 'send\_random\_number', 'min': 1, 'max': 100, 'text': "Here's a random number between 1 and 100: {}"}, {'type': 'back\_to\_start', 'text': 'Back to main menu'}\]}, 'JOKE': {'actions': \[{'type': 'send\_joke', 'text': "Here's a funny joke: {}"}, {'type': 'back\_to\_start', 'text': 'Back to main menu'}\]}}

Чтобы создать модуль генерации кода, который принимает объект BotDefinition и генерирует Python-код с использованием библиотеки aiogram, мы сначала создадим шаблон для кода бота aiogram, а затем используем библиотеку Jinja2 для рендеринга конечного кода с разобранными YAML данными.

Сначала установите Jinja2:

pip install jinja2

Создайте файл шаблона с именем bot_template.py.j2 со следующим содержимым:

import logging
import random
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.middlewares.logging import LoggingMiddleware
from aiogram.types import ParseMode
from aiogram.utils import executor

logging.basicConfig(level=logging.INFO)

API_TOKEN = '{{ bot_token }}'

bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
dp.middleware.setup(LoggingMiddleware())

{% for state, actions in states.items() %}
{% for action in actions['actions'] %}
{% if action['type'] == 'send_message' %}
async def {{ state }}_send_message(message: types.Message):
    await message.reply("{{ action['text'] }}", parse_mode=ParseMode.HTML)
{% endif %}

{% if action['type'] == 'show_keyboard' %}
async def {{ state }}_show_keyboard(message: types.Message):
    keyboard_markup = types.ReplyKeyboardMarkup(row_width=2)
    {% for option in action['options'] %}
    button = types.KeyboardButton("{{ option['text'] }}")
    keyboard_markup.add(button)
    {% endfor %}
    await message.reply("Choose an option:", reply_markup=keyboard_markup)
{% endif %}
{% endfor %}
{% endfor %}

# Register message handlers
{% for state, actions in states.items() %}
@dp.message_handler(commands=['{{ state.lower() }}'])
async def {{ state.lower() }}_command_handler(message: types.Message):
    {% for action in actions['actions'] %}
    await {{ state }}_{{ action['type'] }}(message)
    {% endfor %}
{% endfor %}

async def on_start(dp):
    await bot.send_message(chat_id=config.ADMIN_ID, text='Bot has been started')

async def on_shutdown(dp):
    await bot.send_message(chat_id=config.ADMIN_ID, text='Bot has been stopped')

    await dp.storage.close()
    await dp.storage.wait_closed()

    await bot.session.close()

if __name__ == '__main__':
    from aiogram import executor
    executor.start_polling(dp, on_startup=on_start, on_shutdown=on_shutdown)

Теперь создайте скрипт на Python, который считывает объект BotDefinition и рендерит код бота, используя Jinja2:

from jinja2 import Environment, FileSystemLoader

import parser_bot


def generate_bot_code(bot_definition):
    env = Environment(loader=FileSystemLoader('.'))
    template = env.get_template('bot_template.py.j2')

    bot_token = bot_definition.get_bot_token()
    states = bot_definition.get_states()

    rendered_code = template.render(bot_token=bot_token, states=states)

    with open('generated_bot.py', 'w') as f:
        f.write(rendered_code)


if __name__ == '__main__':
    # First, parse the YAML file to create a BotDefinition object
    bot_definition = parser_bot.parse_bot_definition("example.yaml")

    # Then, generate the bot code using the BotDefinition object
    generate_bot_code(bot_definition)

Этот скрипт использует Jinja2 для рендеринга кода бота aiogram на основе шаблона и разобранных данных YAML. Он записывает сгенерированный код в файл с именем generated_bot.py.
Осталось только добисать в него методы которые мы хотим что бы выполнялись при выборе RANDOM_NUMBER_send_random_number и JOKE_send_joke

Вот так:

import random

async def RANDOM_NUMBER_send_random_number(message: types.Message):
    random_number = random.randint(1, 100)
    await message.reply(f"Here's a random number between 1 and 100: {random_number}", parse_mode=ParseMode.HTML)

@dp.message_handler(lambda message: message.text.lower() == 'show random number')
async def random_number_handler(message: types.Message):
    await RANDOM_NUMBER_send_random_number(message)


async def RANDOM_NUMBER_back_to_start(message: types.Message):
    await start_send_message(message)
    await start_show_keyboard(message)


@dp.message_handler(lambda message: message.text.lower() == 'back to main menu')
async def back_to_start_handler(message: types.Message):
    await RANDOM_NUMBER_back_to_start(message)
import random

async def JOKE_send_joke(message: types.Message):
    jokes = [
        "Why don't scientists trust atoms? Because they make up everything!",
        "Why did the chicken cross the road? To get to the other side!",
        "Why couldn't the bicycle stand up by itself? Because it was two-tired!",
        "Why do we never tell secrets on a farm? Because the potatoes have eyes and the corn has ears!",
        "What's orange and sounds like a parrot? A carrot!"
    ]

    selected_joke = random.choice(jokes)
    await message.reply("Here's a funny joke: {}".format(selected_joke), parse_mode=ParseMode.HTML)

@dp.message_handler(lambda message: message.text.lower() == 'tell a joke')
async def joke_handler(message: types.Message):
    await JOKE_send_joke(message)


async def JOKE_back_to_start(message: types.Message):
    await start_show_keyboard(message)

@dp.message_handler(lambda message: message.text.lower() == 'back to main menu' and current_state == "JOKE")
async def joke_back_to_start_handler(message: types.Message):
    await JOKE_back_to_start(message)

Теперь вы можете запустить этого бота :-)

Вроде бы не простая тема. Но мы быстро управились.

Код на GitHub

Если понравилось, с тебя подписка в соц сетях :)

https://twitter.com/semenov1981
https://t.me/InfoTechPulse

Комментарии (5)


  1. dabrahabra
    09.04.2023 08:55
    +1

    А зачем генерировать код, если его можно интерпретировать?


    1. HarryFox
      09.04.2023 08:55

      Тоже интересует этот вопрос. Почему бы не использовать метапрограммирование в python вместо кодогенерации


  1. ValeriVP
    09.04.2023 08:55
    +2

    Я создал готовое подобное решение ГрафиБот: Графический конструктор телеграм-ботов для 1С https://www.v8-pr.ru/gbc

    Достаточно просто нарисовать блок-схему телеграм бота, и он сразу заработает.


  1. Tishka17
    09.04.2023 08:55
    +1

    Интересная мысль с генерацией python-кода. Так же, мне нравится разделение кастомного кода бизнес логики и описание UI. Единственно, что пока не понятно как оно будет работать в больших ботах со сложной логикой. Сбор всех функций в один класс станет нецелесообразным - лучше разделить их по группам. Кроме того, в ботах есть часто повторяющиеся действия - чекбоксы, выбор из динамического списка вариантов, хранящихся в БД, что сразу просится для унификации. Было бы интересно посмотреть, что из этого выйдет.

    У меня есть проект, тоже позволяющий описывать UI декларативно, но я не стал пока выносить это в YAML, а предлагаю описывать в виде python-кода (хотя ребята показывали как они генерируют мои объекты из yaml). И тут уже есть свобода создания кастомных компонентов, наследования для внедрения своей логики и т.п. Велкам на гитхаб: https://github.com/Tishka17/aiogram_dialog/. И вот поверх этого уже можно генерировать превью, диаграмму переходов (с определенными ограничениями). Простенькое демо нескольких фич доступно тут: https://t.me/aiogram_dialog_demo_bot (код демо).

    Условно у меня это выглядит вот так:

    Dialog(
        Window(
            Const("Greetings! Please, introduce yourself:"),
            StaticMedia(
                path=os.path.join(static_dir, "python_logo.png"),
                type=ContentType.PHOTO,
            ),
            MessageInput(name_handler, content_types=[ContentType.TEXT]),
            MessageInput(other_type_handler),
            state=DialogSG.greeting,
        ),
        Window(
            Format("{name}! How old are you?"),  # dynamic text
            Select(  # dynamic buttons list
                Format("{item}"),
                items="ages",  # retrieved from getter function
                item_id_getter=lambda x: x.id,
                id="w_age",
                on_click=on_age_changed,
            ),
            state=DialogSG.age,
            getter=get_data,
        ),
    )


  1. thunderspb
    09.04.2023 08:55

    Если только как опенсорс. Так что встречал коммерческие решения, где ты просто драг'н'дропом накидывает флоу.