Вам не нужны инструменты для создания чат-ботов – давайте создадим бота Messenger с нуля

1656654139 vam ne nuzhny instrumenty dlya sozdaniya chat botov – davajte sozdadim

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

Но что если вы хотите написать его самостоятельно, с нуля, без использования каких-либо модных инструментов? Это вообще возможно? А можно сделать что-нибудь полезное? Ответ – да, потому что я сделал это, и я собираюсь показать вам, как.

Весь код доступен здесь на Github. Мы будем создавать бота для Facebook Messenger и использовать Google App Engine для размещения нашего бота, который будет написан на Python.

1*NVfzXyBPKYGaNqHk2f208w
Я случайно знаю кое-что о чат-ботах. Фото Скотта Уэбба на Unsplash.

Но подождите, почему вы хотите это сделать? Использовать графический пользовательский интерфейс для создания бота гораздо проще. Что ж, вот несколько причин:

  • Это бесплатно. Бесплатный уровень App Engine настолько щедр, что вряд ли вы превысите его, если у вас не будет много тысяч пользователей-ботов – в таком случае вы будете смеяться.
  • Учить. Посмотрите, что действительно нужно для создания чат-бота.
  • Выйдите за рамки возможностей инструментов для создания чат-ботов. Чувствуете себя амбициозными? Сделайте что-нибудь совершенно оригинальное или создайте собственную платформу для чат-ботов.

Выбор канала чат-бота

Вы можете создать бота для многих разных каналов. Одними из самых популярных являются Facebook Messenger, Kik, Slack, Twitter и Telegram. Если вам нужно поддерживать несколько платформ, вам будет лучше использовать бот-фреймворк. Таким образом, вам не придется писать код для интеграции со всеми платформами, которые вы хотите поддерживать.

В этой статье мы собираемся создать чат-бот для Facebook Messenger. почему Ну, во-первых, это самая популярная платформа для чат-ботов. Почти все инструменты для создания чат-ботов нацелены на Messenger, и их достаточно много только поддержка Messenger. И по уважительной причине: в 2017 году у него было 1,2 миллиарда активных пользователей ежемесячно. Это много потенциальных пользователей чат-ботов.

Есть еще одна причина, почему мы хотим нацелиться на Messenger: скорые ответы. Это кнопки, которые ваш чат-бот может предлагать пользователям в качестве ярлыка, чтобы им не нужно было вводить текст. Они не только делают вашего бота гораздо более привлекательным (кому нравится печатать на мобильном телефоне?), но они также значительно облегчают вашу работу как разработчика чат-бота.

Если вы предлагаете пользователям кнопки, они будут нажимать эти кнопки. Это означает, что вам не нужно беспокоиться об анализе произвольных запросов от пользователей, которые хотят знать, будет ли завтра дождь или где они могут получить пиццу. Направлять пользователей полезно и им, и нам.

Что делать боту?

1*e0z56Y8J_p8KcfaFYS6rvA
Ни один не пройдет. Ну, в большинстве своем ни одного. Фото Джереми Дорроу на Unsplash

Вашему боту нужна цель. Это не может сделать все. Мы с моим другом Наре Варданяном разработали бота, чтобы помогать людям ориентироваться в мутной воде заявок на визу в Великобританию. В этой статье мы будем использовать упрощенную версию этого бота в качестве примера.

Магия обхода дерева

1*xkhS6RGZ1D7OhYdsC64IQA
Не такое дерево. Фото Адарша Куммура на Unsplash.

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

say: "What is the purpose of your visit? (options: travel, study, business/work, medical treatment, join family/get married, visit child at school, diplomatic/government visit)"
answers:
  travel:
    say: You need a Standard Visitor Visa
  study:
    say: How long are you going to stay in the UK? up to 6 months; more than 6 months
    answers:
      up to 6 months:
        say: You can apply for a Short-term Study Visa
      more than 6 months:
        say: You need a Study Visa (Tier 4)
  business/work:
    say: How long are you going to stay in the UK? up to 6 months; more than 6 months
    answers:
      up to 6 months:
        say: You need a Standard Visitor Visa
      more than 6 months:
        say: Are you an 1. entrepreneur 2.investor 3. leader in arts or sciences 4. none of the above
        answers:
          '1':
            say: You can apply for a Tier 1 Entrepreneur
          '2':
            say: You can apply for Tier 1 Investor
          '3':
            say: You can apply for Tier 1 (Exceptional Talent)
          '4':
            say: Are you offered  1. a skilled job 2. role in the UK branch of your employer 3. job in a religious community 4. job as an elite sportsman or coach
            answers:
              '1':
                say: You can apply for a Tier 2 (General) visa
              '2':
                say: You can apply for a Tier 2 (Intra-company transfer)
              '3':
                say: Tier 2 (Minister of Religion)
              '4':
                say: Tier 2 (Sportsperson)
  medical treatment:
    say: You need a Standard Visitor Visa
  join family/get married:
    say: You need a Family of a settled person visa if your family/partner are settled in the UK or a 'dependant' visa of their visa category if they are working or studying
  visiting a child:
    say: You need a Parent visa if you're visiting for over 6 months and a Standard Visitor visa if your visit is  for less than 6 months
  diplomatic or government visit:
    say: You can apply for exempt vignette (exempt from immigration control)

Это упрощенная версия нашего визового консультационного бота в форме дерева. Он в формате YAML (Yet Another Markup Language), что упрощает его чтение. Корневой узел определяет первое сообщение, которое бот посылает пользователю, в этом случае спрашивая пользователя «Какова цель вашего визита?» Дочерние узлы (указанные в разделе «ответы») содержат возможные ответы, которые мы примем, а именно «путешествие», «обучение», «бизнес/работа» и т.д.

Начинаем

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

  • Страница Facebook – каждому боту нужна отдельная страница Facebook.
  • Учетная запись разработчика, позволяющая создавать приложения.
  • Программа Facebook для получения секретного маркера доступа, который потребуется позже.

Боты Facebook работают с веб-хукикоторые являются только URL-адресами, которые Facebook Messenger использует для взаимодействия с вашим ботом.

Чтобы создать веб-хук, мы используем Google App Engine. Преимуществом этого является то, что он бесплатн для небольших объемов и автоматически масштабируется, когда вы получаете больше трафика. Для этой статьи я использовал Python, но есть много других языков, которые вы можете использовать. Вам нужно будет скачать Python SDK и создать облачный проект Google, если у вас его еще нет.

Создание вебхука

1*wvmnYETqWUowkuP5Y2jQPA
Увлекся? Фото Фабьена Базанега на Unsplash.

Первое, что должен сделать наш вебхук, это позволить Facebook проверить, действительно ли мы правильный вебхук. Мы делаем это, обрабатывая запрос GET, содержащий «токен проверки». Это секретная случайная строка, которой мы поделились с Facebook. Эта часть нашего кода базируется на великолепном репозитории Facebook Messenger Bot.

class MainPage(webapp2.RequestHandler):
    def __init__(self, request=None, response=None):
        super(MainPage, self).__init__(request, response)
        logging.info("Initialising with new bot.")
        self.bot = TreeBot(send_message, UserEventsDao(), TREE)

    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        mode = self.request.get("hub.mode")
        if mode == "subscribe":
            challenge = self.request.get("hub.challenge")
            verify_token = self.request.get("hub.verify_token")
            if verify_token == VERIFY_TOKEN:
                self.response.write(challenge)
        else:
            self.response.write("Ok")

Здесь мы поначалу инициализируем класс для обработки запросов в рамках webapp2. Сначала мы регистрируем сообщения о том, что бот инициализируется, а затем создаем класс TreeBot который будет обрабатывать всю логику бота, рассмотренную ниже.

Далее мы проверяем запросы на «подписку» от Facebook и проверяем, токен ли подтверждение, отправленный в запросе, совпадает с секретным, которым мы поделились с Facebook.

Обработка сообщений от пользователей

Далее нам нужно интерпретировать сообщения от пользователей, которые Facebook посылает на наш веб-хук с помощью запросов POST.

    def post(self):
        data = json.loads(self.request.body)
        logging.info("Got data: %r", data)

        if data["object"] == "page":

            for entry in data["entry"]:
                for messaging_event in entry["messaging"]:
                    sender_id = messaging_event["sender"]["id"]

                    if messaging_event.get("message"):
                        message = messaging_event['message']
                        if message.get('is_echo'):
                            logging.info("Ignoring echo event: " + message.get('text', ''))
                            continue
                        message_text = messaging_event['message'].get('text', '')
                        logging.info("Got a message: %s", message_text)
                        self.bot.handle(sender_id, message_text)

                    if messaging_event.get("postback"):
                        payload = messaging_event['postback']['payload']
                        self.bot.handle(sender_id, payload)
                        logging.info("Post-back")

Здесь мы сначала анализируем данные JSON из Facebook и регистрируем их, чтобы помочь с отладкой. Затем мы повторяем события обмена сообщениями в данных. Сначала мы вытаскиваем идентификатор отправителя, который нам понадобится, чтобы отправить ответ пользователю. Существует два типа событий: сообщения (вводимые пользователем) и события «возвращения», которые отправляются, когда пользователь нажимает кнопку быстрого ответа.

Для первого из них нам нужно игнорировать события «эхо». Затем мы вытаскиваем текст сообщения и отправляем его нашему боту для обработки. Мы делаем то же самое с событиями обратной связи, извлекая полезную нагрузку, которая в нашем случае является только текстом кнопки.

Рассылка сообщений пользователям

1*JYGEBAIYB9j-gkfy7iijQg
Сообщения от пользователей не поступают в бутылки. Фото Скотта Ван Хоя на Unsplash.

Когда мы построили наш TreeBot мы передали функцию send_message что позволяет логике бота посылать пользователю соответствующие сообщения. Вот:

def send_message(recipient_id, message_text, possible_answers):
    logging.info("Sending message to %r: %s", recipient_id, message_text)
    headers = {
        "Content-Type": "application/json"
    }
    message = get_postback_buttons_message(message_text, possible_answers)
    if message is None:
        message = {"text": message_text}

    raw_data = {
        "recipient": {
            "id": recipient_id
        },
        "message": message
    }
    data = json.dumps(raw_data)
    r = urlfetch.fetch(" % ACCESS_TOKEN,
                       method=urlfetch.POST, headers=headers, payload=data)
    if r.status_code != 200:
        logging.error("Error sending message: %r", r.status_code)


def get_postback_buttons_message(message_text, possible_answers):
    if possible_answers is not None and len(possible_answers) <= 3:
        buttons = []
        for answer in possible_answers:
            if len(answer) > 20:
                return None
            buttons.append({
                "type": "postback",
                "title": answer,
                "payload": answer,
            })
        return {
            "attachment": {
                "type":"template",
                "payload": {
                    "template_type": "button",
                    "text": message_text,
                    "buttons": buttons,
                }
            }
        }
    return None

Идентификатор получателя будет идентификатором отправителя, который мы извлекли раньше. При этом у нас есть текст сообщения и несколько кнопок быстрого ответа, которые пользователь может нажать. Сначала мы убедимся, что заголовки запроса указывают наше содержимое как JSON, а затем создадим часть сообщения с кнопками возврата. Мы указываем идентификатор получателя и текст сообщения и конвертируем в JSON. Мы делаем запрос на Facebook Graph API, передавая секретный маркер доступа, который Facebook предоставил нам при создании программы.

Запуск бот-сервера

Последний фрагмент кода в этом файле просто создает основной класс и запускает его:

app = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

Мозг ботов

1*HFJOSbIDPjI3SW8Jx4a4Zg
Не мозг, но смотрится так. Фото Власть Чомпалова на Unsplash.

Теперь мы переходим к любопытному: как бот знает, что сказать? Мозги бота в файле bot.py.

class TreeBot(object):
    def __init__(self, send_callback, users_dao, tree):
        self.send_callback = send_callback
        self.users_dao = users_dao
        self.tree = tree

    def handle(self, user, message):
        self.users_dao.add_user_event(user, 'received', message)
        history = self.users_dao.get_user_events(user)
        tree = self.tree
        logging.debug("History items: %d", len(history))
        restarting = False
        nothing_sent = True
        response = DEFAULT
        possible_answers = DEFAULT_POSSIBLE_ANSWERS
        for direction, content in history:
            response = DEFAULT
            possible_answers = DEFAULT_POSSIBLE_ANSWERS
            if direction == 'received':
                key = get_content_match(content, tree)
                if nothing_sent:
                    response = tree['say']
                    possible_answers = tree['answers'].keys()
                elif key is not None:
                    tree = tree[key]
                    if 'say' in tree:
                        response = tree['say']
                        possible_answers = None
                        if 'answers' in tree:
                            possible_answers = tree['answers'].keys()
                    restarting = False
                elif restarting:
                    if content == 'yes':
                        tree = self.tree
                        response = tree['say']
                        possible_answers = tree['answers'].keys()
                        restarting = False
            elif direction == 'sent':
                nothing_sent = False
                if 'answers' in tree and direction == 'sent' and content == tree.get('say'):
                    tree = tree['answers']
                elif direction == 'sent' and content == DEFAULT:
                    restarting = True
            else:
                raise ValueError("Unexpected direction: " + direction)

        logging.debug("Responding: %s", response)

        self.send_callback(user, response, possible_answers)
        self.users_dao.add_user_event(user, 'sent', response)

Класс инициализируется тремя параметрами:

  • функция обратного вызова (определенная выше) для отправки сообщений пользователям
  • объект доступа к данным для хранения информации о пользователях
  • дерево, содержащее логику того, что следует сказать, когда. Это анализируется с YAML, который мы показали выше.

Сначала мы записываем, что получили сообщения пользователя, а затем получаем все предыдущие действия пользователя из объекта доступа к данным. Затем мы воспроизводим действия пользователя, чтобы определить, где он находится в дереве.

Мы инициализируем ответ на сообщение по умолчанию, которое будет возвращаться, когда пользователь скажет, что мы не понимаем. В нашем случае это «Извините, я не понял, начнем снова?» Существует также несколько возможных ответов по умолчанию, а именно да и нет. Мы также ведем учет того, считаем ли мы, что перезапускаем разговор с нуля.

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

def get_content_match(content, tree):
    matches = []
    for key in sorted(tree):
        if content.lower() in key.lower():
            matches.append(key)
    if len(matches) == 1:
        return matches[0]

Это проверяет содержимое сообщения пользователя, чтобы увидеть, встречается ли оно как подстрока одного из текущих параметров в дереве. Если есть одно совпадение, мы возвращаем это совпадение, иначе либо ответ пользователя будет неоднозначным, либо совпадения нет вообще.

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

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

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

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

Наконец, после просмотра всей истории, мы регистрируем наш ответ, посылаем сообщения назад пользователю и записываем посланное сообщение в наш объект доступа к данным.

Последняя часть головоломки

1*a5N4vokpfnlmUnnJMou9zw
Написать чат-бота легче, чем разгадывать эту головоломку. Фотография Ганса-Питера Гаустера на Unsplash.

Единственный фрагмент кода, который остался обсудить, это объект доступа к данным, хранящий все действия пользователя. Мы приняли дизайнерское решение сохранять все действия пользователя и воспроизводить их, как мы делали выше, потому что это позволяло нам легко изменять логику бота и все еще иметь возможность выводить соответствующее состояние для бота и пользователя. Если бы вместо этого мы решили пометить каждый узел дерева и сохранить эту метку, тогда любые изменения в дереве сделали бы старые разговоры недействительными.

Итак, наш объект доступа к данным должен иметь возможность делать две вещи: сохранять новое событие пользователя и получать все события для конкретного пользователя.

class UserEvent(ndb.Model):
    user = ndb.StringProperty()
    direction = ndb.StringProperty()
    message = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)


class UserEventsDao(object):
    def add_user_event(self, user, direction, message):
        event = UserEvent()
        event.user = user
        event.direction = direction
        event.message = message
        logging.info("Adding event: %r", event)
        event.put()

    def get_user_events(self, user):
        events = UserEvent.query(UserEvent.user == user)
        sorted_events = sorted(events, key=lambda x: x.date)
        return [(event.direction, event.message) for event in sorted_events]

Наш объект доступа к данным использует Google Datastore, который очень прост в использовании через App Engine и обладает щедрым уровнем бесплатного использования. Python API делает использование Datastore очень простым. Сначала мы создаем модельный класс, UserEvent определяющий поля и их типы. В нашем случае идентификатор пользователя, направление сообщения и само сообщение являются строчками, и, наконец, дата события имеет тип дата-время.

Чтобы создать и сохранить новое пользовательское событие, мы просто создаем этот класс, устанавливаем свойства, а затем вызываем put() на объекте.

Чтобы получить события для пользователя, мы вызываем query() функцию в классе, передавая пользовательский идентификатор. Затем мы сортируем события по дате и возвращаем список пар направления-сообщения.

Развертывание

Вот и все фишки нашего бота! Теперь необходимо развернуть его и подключить к Messenger.

Чтобы развернуть свою программу в App Engine, воспользуйтесь gcloud команда, поставляемая с пакетом SDK App Engine:

gcloud app deploy --project [YOUR_PROJECT_ID]

После развертывания URL вашего вебхука есть

http://[YOUR_PROJECT_ID].appspot.com/

Обновите свое приложение Facebook с помощью этого URL-адреса веб-хука (следуйте инструкциям здесь), и все будет готово!

Мир – это ваша устрица чат-бота

1*D-NZf2K93B_GCI71CR5jjA
Устрицы – это вкусно, но чат-боты – это весело. Фото Шарлотты Коннибир на Unsplash.

С помощью этих методов можно создать много видов чат-ботов. Как-то мне было весело делать a Выберите свое собственное приключение style bot, но я уверен, что вы сможете придумать гораздо более изобретательные вещи. О, и если вы хотите опробовать визовый бот, вы можете попробовать его здесь (хотя эта версия немного сложнее).

И, если вы не любите всю эту трудную работу, вы всегда можете попробовать один из многих инструментов для создания чат-ботов.

Добавить комментарий

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