Цель:
Разработайте и зарегистрируйте навык для Алисы на сервисе ЯндексюДиалоги;
В качестве backend-a для навыка реализуйте приложение на языке С++ выполняющее следующие функции:
Составление продуктовой корзины:
Вывод справочной информации по навыку;
Регистрацию webhook-ов сторонних сервисов;
Отправку данных на сторонние сервисы.
В качестве стороннего сервиса реализуйте приложение на языке Python выполняющее следующие функции:
Подробности указаны далее.
Общая схема post-запроса представлена на рисунке ниже:

Протокол: http, https.
Хост: доменное имя или ip-адрес сервера. Обычно по одному ip-адресу находится несколько сайтов, в этом случае доменное имя используется сервером, чтобы выбрать правильного получателя запроса.
Сетевой порт: число от 0 до 65535. Своеобразное расширения IP-адреса, нужен, чтобы несколько приложений на компьютере одновременно могли работать с сетью. Запрос приходит на IP-адрес, а дальше операционная система отдаёт его тому приложению, которое слушает порт указанный в запросе. Несколько приложений не могут одновременно слушать один и тот же порт благодаря этому пакет Skype не попадёт к Discord и т.д. Обычно веб сервера слушают 80 и 443 порты. 80 - стандартный порт для http запросов, 443 - для https. Браузеры автоматически дописывают номера портов ориентируясь на название протокола, поэтому пользователю это делать не нужно.
Путь к ресурсу: просто строка. Приложение получившее запрос само решает как реагировать на эту строку. В целом структура пути к ресурсу не отличается у get и post запросов, но обычно у post-запроса путь указывает на какой-то обработчик (скрипт или функцию в web-приложении), а у get-запроса он вполне может указывать на статический файл (картинку, музыку, ...) .
В отличие от get-запросов post-запросы предназначены для отправки данных. Обычно именно этот метод выбирается для отправки данных html-форм, т.к. значения полей не отобразятся в адресной строке браузера и точно не попадут в кэш.
О том ,что такое вебхук можно прочитать тут.
До сих пор в С++ не предусмотрен стандартный способ работы со строками в формате utf-8, но данный стандарт кодирования символьной информации сейчас настолько распространён, что по факту является общепринятой нормой хранить и обмениваться текстом именно в utf-8.
В данной лабораторной работе мы будем обходится стандартными возможностями языка.
Для хранения и работы со строками будем использовать std::string.
Любой строковой литерал в коде должен быть записан с префиксом u8.
std::string str1 = u8"Привет, мир"; // Обчная строкаif (str1 == u8"Привет, мир") std::cout << 1; // Сравнение строкstd::string str1 = u8R"(Привет "Мир дикого запада")"; // Сырой строковой литералПоиск/замена/сравнение на равенство будут работать корректно, если всё будет в utf-8;
С выводом в терминал есть проблемы на windows. Большинство решений доступных на сегодняшний день не универсальны и зависят как от версии OC, так и от компилятора. Простейшим решением, в данном случае может быть сохранение вывода в файл и просмотр через текстовый редактор. Например Sublime Text, при получении фокуса, автоматически перезагружает содержимое файла, если он изменился.
В коде достаточно создать глобальный файловый поток и в дальнейшем пользоваться им вместо cout:
xxxxxxxxxxstd::ofstream logger("log.txt"); // Где-то дальше по кодуlogger << u8"Произошло событие" << std::endl;
Серверная часть отвечает за взаимодействие с Алисой, а так же за обработку и дальнейшую пересылку данных подчинённым сервисам. Язык серверного приложения: С++.
Для работы с сетью используйте библиотеку: https://github.com/yhirose/cpp-httplib.
Клонируйте или скачайте в виде архива репозиторий библиотеки https://github.com/yhirose/cpp-httplib;
В папке include проекта создайте папку cpp_httplib и скопируйте туда файл httplib.h из скаченного ранее репозитория. В дальнейшем сам репозиторий больше не потребуется, его можно удалить;
Скопируйте и вставьте в главный .cpp файл следующий код:
xxxxxxxxxx using namespace httplib; // В этой функции формируем ответ сервера на запросvoid gen_response(const Request& req, Response& res) { // Выводим на экран тело запроса std::cout << req.body.c_str(); // Здесь будет ответ, пока-что взят пример из документации std::string str = u8R"( { "response": { "text": "Здравствуйте! Это мы, хороводоведы.", "tts": "Здравствуйте! Это мы, хоров+одо в+еды.", "buttons": [ { "title": "Надпись на кнопке", "payload": {}, "url": "https://example.com/", "hide": true } ], "end_session": false }, "version": "1.0" })"; // Отправляем ответ res.set_content(str, "text/json; charset=UTF-8");} int main() { Server svr; // Создаём сервер (пока-что не запущен) svr.Post("/", gen_response); // Вызвать функцию gen_response на post запрос std::cout << "Start server... OK\n"; // cout использовать нельзя svr.listen("localhost", 1234); // Запускаем сервер на localhost и порту 1234}Обратите внимание, что в отличие от прошлой лабораторной работы, в этом примере сервер отвечает не на get, а на post-запрос.
При попытке перейти по ссылке http://localhost:1234/ мы не получим от сервера никакого ответа, т.к. запрос будет отправлен методом get. Для быстрой проверки работоспособности кода вставьте в адресную строку браузера следующее:
xxxxxxxxxxdata:text/html,<form action=http://localhost:1234 method=post><input name=key></form>в появившемся поле введите любой текст и нажмите Enter.
В дальнейшем нам потребуется работать с JSON. Для этого используйте библиотеку: https://github.com/nlohmann/json.
json.hpp отсюда: https://github.com/nlohmann/json/releases. Его также можно найти в репозитории проекта https://github.com/nlohmann/json в папке: single_include/nlohmann.include создайте папку nlohmann и скопируйте туда файл json.hpp.#include <nlohmann/json.hpp> и using json = nlohmann::json;Взаимодействовать с подчинёнными сервисами будем посредством Webhook-ов. Для этого понадобится способ их регистрации и удаления во время работы сервера.
Сразу после запуска сервер должен проверить наличие конфигурационного файла. Если файла нет, то создать (такой как указан ниже), если есть, загрузить.
Конфигурационный файл - это текстовый файл в формате JSON следующей структуры:
xxxxxxxxxx{ "webhooks":[]}В массиве webhooks перечислены все зарегистрированные вебхуки.
Если серверу приходит get-запрос на /webhooks, сервер должен заполнить и отдать html-шаблон. В шаблоне нужно заменить заглушку {webhooks_list} на пустую строку, если ещё нет зарегистрированных webhook-ов или на блок указанный ниже если есть. Блок нужно повторить столько раз, сколько webhook-ов, при этом вместо {Webhook URL} должен быть конкретный webhook.
xxxxxxxxxx<div class="form-row align-items-center"> <div class="col"> <input type="text" value="{Webhook URL}" class="form-control mb-2" disabled> </div> <div class="col"> <button type="submit" name="del" value="{Webhook URL}" class="btn btn-danger mb-2">Удалить</button> </div></div>Пример как должен выглядеть заполненный шаблон с тремя зарегистрированными webhook-ами.
Нажатие на любую из кнопок на странице приводит к отправке post-запроса на /webhooks. Если нажата кнопка Принять в запрос добавляется параметр set со значением равным тексту указанному в поле ввода которое вводится пользователем. Если нажата кнопка Удалить, в запрос добавляется параметр del со значением из соседнего поля ввода.
При нажатии на кнопку (post-запрос) сервер должен отреагировать добавив новый webhook в список или удалив выбранный. Ответом на запрос должна быть обновлённая страница и все изменения должны быть сохранены в конфигурационный файл. Могут пригодиться:
xxxxxxxxxx// Узнать какой запрос пришёлreq.method == "GET"; // true или falsereq.method == "POST"; // true или false// Узнать есть ли параметр в запросеreq.has_param("del"); // true или false// Получить значение параметра из запросаauto val = req.get_param_value("del");// Удалить данные из json-объекта по ключу. Удаляет весь массивj.erase("webhooks");// Удалить данные из json-массива по индексу. Удаляет элемент с индексом ij["webhooks"].erase(j["webhooks"].begin() + i);В дальнейшем, по заданию, вам потребуется отсылать данные на все зарегистрированные webhook-и. Обычно это делается методом post. Для проверки корректности отправки данных на webhook воспользуемся ресурсом https://webhook.site
Перейдите по указанной ссылке, в результате вы попадёте на страницу где для вас будет сгенерирован уникальный webhook URL (Your unique URL);
В главном меню страницы, справой стороны нажмите кнопку Edit. В появившемся окне в поле Response body введите: OK и нажмите Save. Теперь на любой запрос сервис будет отвечать текстом: "OK";
Создайте пустое консольное приложение, подключите библиотеку cpp_httplib и скопируйте и вставьте в главный .cpp файл следующий код:
xxxxxxxxxxusing namespace httplib; int main() { // Создаём клиент и привязываем к домену. Туда пойдут наши запросы Client cli("http://webhook.site"); // Отправляем post в теле которого будет текст {"Hello": "world"} auto res = cli.Post("/bd748520-3009-4c3e-9323-affda0b34391", R"({"Hello": "world"})", "text/json"); // res преобразуется в true, если запрос-ответ прошли без ошибок if (res) { // Проверяем статус ответа, т.к. может быть 404 и другие if (res->status == 200) { // В res->body лежит string с ответом сервера std::cout << res->body << std::endl; } else { std::cout << "Status code: " << res->status << std::endl; } } else { auto err = res.error(); std::cout << "Error code: " << err << std::endl; }}В примере замените данные на ваши (из Your unique URL). Убедитесь, что в коде протокол http иначе ничего не получится.
Запустите код. В терминале вы должны увидеть сообщение "OK", а на странице https://webhook.site данные из вашего запроса.
Примечание: обратите внимание, что webhook URL, кроме домена, может содержать ещё и путь отличный от корня (как в данном примере), но из за особенностей библиотеки cpp_httplib нельзя указать полный URL в одном месте, домен и путь указываются в разных местах кода.
Для взаимодействия с Алисой нам понадобится URL. В этом нам поможет ngrok:
Forwarding (тот, у которой протокол https);
Создайте или используйте существующий аккаунт на Яндексе (заведите почту).
Перейдите на сервис Яндекс.Диалоги: https://dialogs.yandex.ru/
На главной странице нажмите кнопку Консоль или Создать навык, после чего вы попадёте на dashboard со всеми созданными навыками. В начале там будет пусто;
Нажмите кнопку Создать диалог, а затем Навык в Алисе и вы попадёте на страницу настройки навыка;
Заполните поля формы (документация):
Нажмите Сохранить, а затем Опубликовать.
Запустите серверное приложение (код из пункта I.3);
Перейдите в раздел навыка Тестирование и напишите любое сообщение в чат. Если ответ приходит, значит всё настроено правильно.
Изучите документацию: Протокол работы навыка.
Сервер отвечает на периодическую проверку от сервиса Яндекс.Диалоги.
При старте сессии (["session"]["new"]) сервер отправляет приветственное сообщение содержащее:
Текст: "Здравствуйте! Я помогу вам с покупками.";
Текст дублируется голосом;
Выводятся две кнопки с текстом "Помощь" и "Молчать".
Сессионные данные по умолчанию:
Если сессия не новая:
Достаёт сессионные данные. Если их нет или они не валидные:
Анализирует команду и режим работы навыка. В зависимости от чего и выбирает дальнейшие действия;
Возможные запросы к навыку и алгоритм их обработки:
Пользователь сказал или нажал кнопку "Молчать". Реакция:
Пользователь сказал или нажал кнопку "Говорить". Реакция:
Пользователь сказал или нажал кнопку "Помощь":
Режим работы навыка переключается в режим помощи;
Отправляется стартовое сообщение режима помощи, содержащее:
Если названо/нажата кнопка с коротким названием возможности:
Если пользователь захотел выйти из помощи:
Возможность Корзина.
Команда "Очистить корзину". Реакция:
Команда "Добавить в корзину". Для простоты полагаем, что:
То есть типичная команда выглядит так: "Добавить в корзину огурцы 20 рублей". Реакция:
Команда "Удалить из корзины". Пользователь должен назвать товар точно так же как и при добавлении. То есть "Огурец" и "Огурцы" - разные товары. Реакция:
Если товара нет в корзине:
Если товар есть в корзине:
Команда "Что в корзине". Реакция:
Если в корзине нет товаров:
Если в корзине есть товары:
Команда "Сумма". Реакция:
Команда "Покупка завершена". Реакция:
Если в корзине есть хоть один товар, отправляет всем подписавшимся (всем webhook-ам) post-запрос с json в следующем формате:
xxxxxxxxxx{ "user_id": "9359F683B13A18A1", "check":[ { "item": "сандальки", "price": 250 }, { "item": "носки", "price": 100 } ]}Где "user_id" содержит значение из ["session"]["user"]["user_id"] полученного от Алисы, если оно есть, иначе строку "anonymous". "check" - массив всех товаров с ценами из чека.
Если поступила неизвестная команда, сервер отправляет сообщение, в котором:
Примечание: Считается, что пользователь всегда даёт команды в нужной словоформе, то есть "Помощь" и "Помоги" - разные команды. Навык должен реагировать только на правильную команду, дополнительные вариации предусматривать не обязательно (по желанию).
Клиентское приложение предназначено для получения данных от сервера и сохранения их в excel-документ. Язык клиентского приложения: Python.
Для работы с Excel-документом удобно использовать модуль openpyxl. Модуль openpyxl можно установить при помощи pip :
xxxxxxxxxxpip install openpyxl
Изучите документацию с официального сайта: https://openpyxl.readthedocs.io/en/stable/
Работа с openpyxl продемонстрирована в этих видео: запись, чтение.
В отличие от предыдущей лабораторной работы клиентское приложение будет не отправлять запрос и обрабатывать ответ, а наоборот, ожидать пока к нему обратятся, то есть будет действовать по серверной логике. Модуль requests в данной ситуации нам не подойдёт.
Воспользуемся микрофреймворком Flask. Небольшой вводный видео курс.
Установите Flask выполнив команду:
xxxxxxxxxxpip install flask
Создайте пустой скрипт, затем скопируйте и вставьте туда следующий код:
xxxxxxxxxxfrom flask import Flask app = Flask(__name__) .route('/')def index(): return "Hello, World!" if __name__ == "__main__": app.run()В результате запустится сервер, который будет слушать 5000 порт и на get-запросы к корню "сайта" будет вызывать функцию index . Всё, что вернёт функция index будет отправлено в ответ на запрос. Для проверки, перейдите по адресу: http://localhost:5000/.
По заданию клиентское приложение должно реагировать на post-запросы. Модифицируем код:
xxxxxxxxxxfrom flask import Flask, request app = Flask(__name__) .route('/', methods=['POST', 'GET'])def index(): if request.method == 'POST': return "Это POST запрос" if request.method == 'GET': return "Это GET запрос" if __name__ == "__main__": app.run()Т.к. клиентское приложение будет получать данные от серверного приложения в формате json, то нам потребуется их достать из тела запроса. Для этого у request есть методы json и get_json. Обратите внимание, что данные методы отработают корректно, только в том случае, если у посылаемых данных будет установлен mimetype как application/json. При любом другом и даже text/json результат будет None.
Пример работы в flask с данными в формате json можно посмотреть в этом видео.
Алгоритм работы клиентского приложения:
Клиентское приложение взаимодействует с сервером посредством webhook, подписываясь на событие: пользователь завершил покупку. Само приложение ничего не регистрирует, нужно это сделать руками.
Приложение ждёт post-запрос к корню ("/").
Когда запрос приходит, то:
Когда количество записей в буфере превосходит 1000, данные сохраняются в excel-таблицу с именем data.xlsx и буфер очищается.
Сохранение в excel-таблицу происходит по алгоритму:
data.xlsx существует, то новые данные дописываются в конец таблицы.Формат таблицы:
| N | User ID | Datetime | Item | Prise |
|---|---|---|---|---|
Где "N" - номер записи в таблице начиная с 1 и далее с шагом 1; "User ID" - данные из поля user_id взятые из входящего json; "Datetime" - дата и время. Вычисляются на клиенте в момент получения данных (например так); "Item" - название товара; "Prise" - цена товара.
Отчёт по лабораторной работе оформляется в соответствии с указанными в разделе Правила оценивания требованиями.
В отчёте создайте раздел (заголовок второго уровня) Постановка задачи и продублируйте туда соответствующий блок из этого документа.
Создайте раздел (заголовок второго уровня) Выполнение работы и текстом подробно опишите всё, что делали в процессе выполнения. В описании обязательно должны присутствовать:
В папке с лабораторной работой должно быть: