Pull to refresh

Всепротокольный бот на PHP за 10 минут, или как Microsoft Bot Framework и Azure Functions облегчают нам жизнь

Reading time 12 min
Views 25K
Абсолютно невозможно отрицать, что развитие естественных паттернов в интерфейсах дало фантастический толчок к развитию всей ИТ-индустрии в целом. И речь не только и не столько о голосовых интерфейсах, сколько о повсеместном внедрении жестов, гигантском сдвиге в парадигме мобильных платформ и, конечно, существенных работах в области UI и UX в целом. В то время как индустрия стремится стать все более дружелюбной для все более широких масс людей, обычная и, в определенной степени, рутинная разработка превращается в бесконечные попытки объять необъятное. Если раньше нас в основном заботили уровни абстракции языков и фреймворков, то сейчас перед нами стоят куда более глобальные вопросы. Как найти баланс между сложным и функциональным интерфейсом? Стоит ли начинать новый проект с микросервисов? На эти вопросы я не могу ответить, зато я могу рассказать вам об инструментах, которые уже сейчас существенно облегчают и удешевляют освоение и применение новых технологий и подходов к разработке.

Введение


У чат-ботов длинная и не очень успешная история, но, как и про тач-интерфейсы или нейронные сети, про них можно сказать – всему свое время. В последнем витке истории боты обязаны своей популярностью азиатским мессенджерам и социальным сетям. Получив старт в Азии, они отправились покорять западные социальные сервисы. Те, в свою очередь, наспех состряпав API, стали соревноваться в разнообразии возможностей и размере потенциального заработка на их платформах. Что, превратило жизнь обычных разработчиков, перед которыми встали те же задачи по освоению ботов, но со стороны поставщиков контента и услуг, в сущий кошмар.


Одних только мессенджеров, потенциально выходящих на клиента, можно с тем или иным успехом насчитать с десяток, а ведь есть еще классические СМС, Email и веб-чаты. И все это требует специального подхода, ведь каждый предлагает свой собственный API или SDK. И хорошо бы умудриться покрыть всех одним более или менее консистентным кодом, чтобы не разрабатывать и не поддерживать десяток разных реализаций одного и того же. Бизнес требует не столько качественное покрытие меньших каналов продаж, сколько количественное покрытие большего числа каналов. Даже, если одни каналы приносят больше денег, чем другие. Или, если одни каналы предлагают больше возможностей, чем другие.

Так как же покрыть максимально большее число каналов, не превращая свою работу в бесконечную беготню между разными реализациями одного и того же? Одно из доступных решений — воспользоваться фреймворком. Это, конечно, не решит всех проблем, но сохранит, как минимум, психическое равновесие и, в идеале, деньги заказчика или работодателя.

Microsoft Bot Framework


Microsoft Bot Framework был впервые представлен год назад на конференции Microsoft Build 2016. Однако, он до сих пор находится в стадии Preview, что может создать определенные трудности разного толка с использованием его в больших проектах в текущем виде. По крайней мере, я точно не могу рекомендовать использовать его в продакшн прямо сейчас.

Я не буду рассказывать обо всех возможностях и преимуществах этого фреймворка, вы можете легко узнать об этом из его документации или любой другой статьи о нем. В рамках же этого материала я хочу сделать небольшое погружение в практическую реализацию бота для Microsoft Bot Framework.

Может показаться, что бот – это в принципе сложно, а с незнакомым фреймворком и подавно, но это не так. Как и заявлено в заголовке, реализация самого простого бота с Microsoft Bot Framework займет у вас не более 10-и минут. Примерно столько заняло у меня написание и отладка кода для этой статьи и вдвое больше ушло на поиск подходящего шаблона для его запуска в Azure Functions.

Почему PHP?


Я выбрал PHP для этого материала по ряду причин. Во-первых, потому что основное направление моей работы — веб-разработка, а PHP, по крайней мере в России, остается самым популярным языком в этой области. Я думал о том, чтобы включить в пример и код на Python’е, но решил не усложнять и без того раздувшийся материал. Если это будет интересно, можно вынести в отдельную статью. Во-вторых, Microsoft хоть и заявляет, что с Bot Framework можно работать на PHP или Python’е, но фактически старательно игнорирует их, не даёт никакой документации или примеров, нет даже SDK. Все, что есть – это REST API и его документация. Реализация на совести разработчика, который скорее всего проигнорируют плохо документированную для его языка технологию, какой бы чудесной и простой она не была. Примеры на других языках не всегда читаются легко, особенно если они перегружены чуждыми конструкциями, вроде «async/await».

Почему Azure Functions?


Я выбрал Azure Functions, потому что это самый простой и дешевый способ для любого читателя – попробовать и поупражняться с кодом этого примера. Фактически, все, что от вас требуется для запуска полноценного бота из этого материала – это нажать кнопку «Deploy to Azure» и ввести пару параметров для приложения. Azure сам создаст и сконфигурирует необходимые ресурсы, загрузит код примера из GitHub и установит биллинг таким образом, чтобы вам не пришлось платить за все время работы приложения. Вы будете оплачивать только непосредственные вызовы бота, фактически сообщения, которые он обрабатывает.

Кроме того, если вам интересна тема микросервисов с бессерверной (serverless) архитектурой и вы еще не знакомы с Azure Functions, то это станет для вас отличным поводом для знакомства. Однако, микросервисы и роль Azure Functions в них – это другая тема для другой статьи.

Реализация


Отдельно отмечу, что этот пример намеренно написан самым простым, линейным, хорошо читаемым и наглядным способом. Безусловно, его нельзя применять в качестве рабочего кода. Так как для PHP на данный момент нет никакого SDK, усложнение этого примера классами и отдельными функциями было бы совершенно излишним и только усложнило его читаемость и восприятие.

Подготовка


Итак, первое, что вам нужно сделать, это создать самого бота: https://dev.botframework.com/bots/new. Придумайте боту имя (Name), описание (Description) и альяс для именования в URL’ах (Bot handle). Учтите, что в отличие от имени и описания, альяс после создания бота изменить нельзя. Оставьте поле «Messaging endpoint» пока пустым, мы заполним его после запуска приложения.


Нажмите кнопку «Manage Microsoft App ID and password», чтобы создать идентификатор и пароль для вашего приложения. ID и пароль нужны для авторизации приложения в качестве бота при отправке сообщений в чат, поэтому сохраните их. Нажмите кнопку «Завершить операцию и вернуться к Bot Framework».


На этом подготовительная часть почти закончена. Ознакомьтесь, подтвердите свое согласие со всеми соглашения и нажмите кнопку «Register». Если все указано верно, вы очень скоро увидите сообщение «Bot created». Номинально, ваш бот готов. Осталось научить его общаться.

Код


Первым делом устанавливаем ID и пароль приложения для аутентификации бота. Вы получили их на прошлом шаге. Заодно устанавливаем URL, куда будем обращаться с ними за токеном, авторизующим ответы бота.

$client_id = getenv('MicrosoftAppId');
$client_secret = getenv('MicrosoftAppPassword');
$authRequestUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';

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

$client_id = ‘26XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX’;
$client_secret = ‘CJPXXXXXXXXXXXXXXXXXXXXXXXXXXXX’;
$authRequestUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';

Далее читаем тело POST-запроса с сообщением от пользователя и десериализуем его из JSON-формата в массив $deserializedRequestActivity.

$request = file_get_contents(getenv('req'));
$deserializedRequestActivity = json_decode($request, true);

В этом примере я получаю тело запроса из переменной окружения req, что преднастроено в биндингах Azure Functions. Вы можете получать тело запроса из потока php://input, если не используете Azure Functions.

$request = file_get_contents('php://input');
$deserializedRequestActivity = json_decode($request, true);

Если $deserializedRequestActivity содержит поле id, считаем входящий запрос корректным и начинаем обработку. Прежде всего, нужно получить токен для авторизации ответа на сообщение. Токен можно получить через POST-запрос к oAuth сервису Microsoft. Я использую stream context для запроса только потому, что его реализация выглядит нагляднее, вы можете использовать CURL.

$authRequestOptions = array(
    'http' => array(
        'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
        'method'  => 'POST',
        'content' => http_build_query(
            array(
                'grant_type' => 'client_credentials',
                'client_id' => $client_id, //ID приложения
                'client_secret' => $client_secret, //Пароль приложения
                'scope' => 'https://graph.microsoft.com/.default'
            )
        )
    )
);

Создаем сконфигурированный выше stream context и выполняем из него запрос к oAuth-сервису.

$authRequestContext  = stream_context_create($authRequestOptions);
$authResult = file_get_contents($authRequestUrl, false, $authRequestContext);

Читаем ответ на запрос и десериализуем его в массив $authData.

$authData = json_decode($authResult, true);

Если $authData содержит поле access_token, считаем аутентификацию успешной и продолжаем обработку. Определяем, какой тип сообщения мы получили и готовим текст ответа на сообщение в случае, если тип входящего сообщения message. В этом примере мы не будем обрабатывать все прочие типы сообщений, поэтому всем остальным типам просто говорим, что мы не знакомы с ними.

switch ((string)$deserializedRequestActivity['type']) {
    case 'message':
        $message = 'New message is received: ' . (string)$deserializedRequestActivity['text'];
        break;
    default:
        $message = 'Unknown type';
        break;
}

Теперь, когда мы знаем, что отвечать, формируем массив $deserializedResponseActivity с данными ответа, который позже передадим в Microsoft Bot Framework.

$deserializedResponseActivity = array(

    //Мы отвечаем обычным сообщением
    'type' => 'message',

    //Текст ответа на сообщение
    'text' => $message,

    //Говорим, что ответ - это простой текст
    'textFormat' => 'plain', 

    //Устанавливаем локаль ответа
    'locale' => 'ru-RU', 

    //Устанавливаем внутренний ID активности, в контексте которого мы находимся (берем из поля id входящего POST-запроса с сообщением)
    'replyToId' => (string)$deserializedRequestActivity['id'],  

    //Сообщаем id и имя участника чата (берем из полей recipient->id и recipient->name входящего POST-запроса с сообщением, то есть id и name, которым было адресовано входящее сообщение)
    'from' => array(
        'id' => (string)$deserializedRequestActivity['recipient']['id'], 
        'name' => (string)$deserializedRequestActivity['recipient']['name']
),

    //Устанавливаем id и имя участника чата, к которому обращаемся, он отправил нам входящее сообщение (берем из полей from->id и from->name входящего POST-запроса с сообщением)
    'recipient' => array(
        'id' => (string)$deserializedRequestActivity['from']['id'],
        'name' => (string)$deserializedRequestActivity['from']['name']
    ),

    //Устанавливаем id беседы, в которую мы отвечаем (берем из поля conversation->id входящего POST-запроса с сообщением)
    'conversation' => array(
        'id' => (string)$deserializedRequestActivity['conversation']['id'] 
    )
);

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

https://{activity.serviceUrl}/v3/conversations/{activity.conversation.id}/activities/{activity.id}

Где activity — это входящий POST-запрос с сообщением, десериализованный ранее в массив $deserializedRequestActivity.

Пропускаем {activity.serviceUrl} через rtrim, чтобы исключить последний закрывающий слеш, потому что иногда он есть, а иногда его нет. Также {activity.id} необходимо пропустить через urlencode, потому что в нем встречаются специальные символы, которые ломают URL и мешают выполнить запрос.

$responseActivityRequestUrl = rtrim($deserializedRequestActivity['serviceUrl'], '/') . '/v3/conversations/' . $deserializedResponseActivity['conversation']['id'] . '/activities/' . urlencode($deserializedResponseActivity['replyToId']);

URL готов, теперь готовим POST-запрос к Microsoft Bot Connector API, в котором передадим ответ на входящее сообщение. Первым делом конфигурируем stream context. Я использую stream context для запроса только потому, что его реализация выглядит нагляднее, вы можете использовать CURL.

$responseActivityRequestOptions = array(
    'http' => array(
        //Устанавливаем в заголовок POST-запроса данные для авторизации ответа, тип токена (token_type) и сам токен (access_token)
        'header'  => 'Authorization: ' . $authData['token_type'] . ' ' . $authData['access_token'] . "\r\nContent-type: application/json\r\n",
        'method'  => 'POST',
        //В тело запроса вставляем сериализованный в JSON-формат массив с данными ответа $deserializedResponseActivity
        'content' => json_encode($deserializedResponseActivity)
    )
);

И сразу же его создаем и выполняем из stream context’а сконфигурированный запрос к Microsoft Bot Connector API.

$responseActivityRequestContext  = stream_context_create($responseActivityRequestOptions);
$responseActivityResult = file_get_contents($responseActivityRequestUrl, false, $responseActivityRequestContext);

Готово, пользователь в чате читает наш ответ, а мы пишем в поток STDOUT лог о получении и обработке очередного сообщения.

fwrite(STDOUT, 'New message is received: "' . (string)$deserializedRequestActivity['text'] . '"');

Запуск


Вышеописанный код лежит на GitHub, нет никакой необходимости копипастить его отсюда или писать заново. Вы можете развернуть код в своем собственном окружении, либо воспользоваться готовым шаблоном для Azure Functions. Если вы собираетесь разворачивать код в собственном окружение, жмите сюда, чтобы пропустить часть про Azure Functions.



Чтобы выполнить автоматическое развертывание кода в Azure, просто нажмите кнопку «Deploy to Azure», расположенную чуть выше. Мастер развертывания запросит у вас только «Microsoft App Id» и «Microsoft App Password», смело вводите те, что получили на этапе подготовки. Обязательно измените значение поля «App Name», так как указанное в шаблоне имя уже используется, и развертывание закончится ошибкой, если оставить его как есть. Просто добавьте туда пару случайных цифр. Дополнительно можете выбрать существующую группу ресурсов или создать новую, если хотите. В остальном параметры лучше оставить как есть. Не забудьте ознакомиться и согласиться с условиями, затем жмите «Приобрести».


Последний штрих, нужно взять ссылку на развернувшееся приложение и воткнуть ее в поле «Messaging endpoint» созданного на этапе подготовки бота.


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


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


Слева под именем функции «messages» перейдите в меню «Разработка» и в открывшемся справа окне жмите ссылку «Get Function URL». В попапе будет ссылка следующего вида.

https://{имя_вашего_приложения}.azurewebsites.net/api/messages?code=JqXXXXXXXXXXXXXXX...

Параметр code защищает ваше приложение от случайных и злонамеренных срабатываний. Не зная этого ключа, нельзя заставить приложение работать, а значит нельзя перегрузить ее запросами и вогнать вас в долги перед Azure. Поэтому постарайтесь никому этот параметр не показывать.

Копируйте ссылку, возвращайтесь в список ваших ботов на портале фреймворка, оттуда зайдите в редактирование бота и вставьте ее (или аналогичную на скрипт messages/run.php из примера, если вы не использовали Azure Functions) в поле «Messaging endpoint».


Проверка и отладка


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

Если вы развернули код из шаблона Azure Functions, то не пугайтесь, если на ваши первые сообщения ответы придут с некоторой задержкой. Это связано с режимом работы инстанса «Службы приложения». Для снижения ваших затрат на хостинг примера, в шаблоне включен так называемый «План потребления», в котором служба потребляет ресурсы по мере необходимости. Проще говоря, она запускается и масштабируется только тогда, когда в этом есть реальная необходимость. Если еще проще, служба будет в состоянии stand by до тех пор, пока ее веб-хук не дернет кто-нибудь, имеющий на это право. Если в процессе выполнения ей не хватит доступных ресурсов, она смаштабируется автоматически. Таким образом, вы сэкономите часы вычисления, но обратите внимание, что это не единственный ресурс, который вы оплачиваете. После обработки вызова и по истечении некоторого времени, служба снова «уснет».

Если же вы уже отправили боту несколько сообщений и не получили от него никакого ответа, то перво-наперво посетите инстанс «Службы приложений». Там находятся журнал вызовов службы (в том числе и с данными из потока STDOUT) и инструмент для тестирования кода.


По умолчанию код будет слинкован с репозиторием примера на GitHub в режиме непрерывного развертывания. Поэтому в консоли вы увидите предупреждение о доступности кода только для чтения. Чтобы изменить код, вам нужно либо совсем отключить непрерывное развертывание, либо клонировать репозиторий и перелинковать службу на него. И то, и другое можно сделать, если перейти из меню консоли слева в «Параметры приложения-функции» и там нажать на кнопку «Настроить непрерывную интеграцию».

Подключение каналов


По умолчанию к боту будут подключены Skype и веб-чат. Дополнительно подключаются как мессенджеры вроде Telegram’а и Slack’а, так и Email или SMS каналы. Есть также REST API интерфейс Direct Line, который позволяет вам использовать свои собственные приложения-чаты или мессенджеры. Настройки всех дополнительных каналов сопровождаются подробной инструкций и, как правило, не вызывают никаких затруднений, поэтому не вижу смысла здесь вдаваться в детали этих процедур.

«Адреса и явки» бота из данного примера:


Заключение


Не секрет, что Microsoft работает над естественными интерфейсами уже очень давно и на данный момент добилась если и не больших, то точно не меньших успехов, чем соратники по цеху. На мой взгляд, этот фреймворк прекрасный тому пример. Он предлагает одно из широчайших покрытий каналов с относительно минимальными трудозатратами. Кроме того, его возможности не сводятся к работе с общими для всех мессенджеров фичами и прямыми чатами. Microsoft Bot Framework позволяет работать с фичами, уникальными для того или иного мессенджера, а также работать в групповых чатах с присущей им логикой. Конечно, он далеко не единственный, и у кого-то может быть другое мнение на этот счет.

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

Но этого совершенно точно недостаточно для поручения боту ваших клиентов. Когда мы говорим о естественных интерфейсах, мы подразумеваем не только формат взаимодействия с пользователем, но и то, как это взаимодействие будет им ощущаться, тот самый UX. Как показывает практика, пользователь не очень-то любит общаться с ботами, которые воспринимают только сухие команды и не способны ответить ни на один вопрос. И на эту проблему у Microsoft есть довольно элегантное решение Microsoft Cognitive Services.

Microsoft Cognitive Services и работа с естественной речью – очень большая и важная часть, которой можно было бы продолжить тему этой статьи. Но, в первую очередь мне хочется услышать, насколько вам это интересно в контексте веб-разработки? Стоит ли из этого делать цикл статей? Примеры на каких языках вам бы хотелось видеть в будущем?

Ссылки по теме


Only registered users can participate in poll. Log in, please.
Мне интересна эта тема в констексте веб-разработки
80.95% Да 85
15.24% Пока не понял 16
3.81% Нет 4
105 users voted. 21 users abstained.
Only registered users can participate in poll. Log in, please.
Мне бы хотелось увидеть цикл статей на эту тему
77% Да 77
18% Пока не понял 18
5% Нет 5
100 users voted. 19 users abstained.
Only registered users can participate in poll. Log in, please.
Мне хочется увидеть примеры на
75.89% PHP 85
23.21% Python 26
16.96% Другой язык, напишу в комментариях 19
112 users voted. 19 users abstained.
Tags:
Hubs:
+26
Comments 10
Comments Comments 10

Articles