Цель:
Разработайте и зарегистрируйте навык для Алисы на сервисе ЯндексюДиалоги;
В качестве 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
:
xxxxxxxxxx
std::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. Для быстрой проверки работоспособности кода вставьте в адресную строку браузера следующее:
xxxxxxxxxx
data: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 или false
req.method == "POST"; // true или false
// Узнать есть ли параметр в запросе
req.has_param("del"); // true или false
// Получить значение параметра из запроса
auto val = req.get_param_value("del");
// Удалить данные из json-объекта по ключу. Удаляет весь массив
j.erase("webhooks");
// Удалить данные из json-массива по индексу. Удаляет элемент с индексом i
j["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
файл следующий код:
xxxxxxxxxx
using 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
:
xxxxxxxxxx
pip install openpyxl
Изучите документацию с официального сайта: https://openpyxl.readthedocs.io/en/stable/
Работа с openpyxl
продемонстрирована в этих видео: запись, чтение.
В отличие от предыдущей лабораторной работы клиентское приложение будет не отправлять запрос и обрабатывать ответ, а наоборот, ожидать пока к нему обратятся, то есть будет действовать по серверной логике. Модуль requests в данной ситуации нам не подойдёт.
Воспользуемся микрофреймворком Flask. Небольшой вводный видео курс.
Установите Flask выполнив команду:
xxxxxxxxxx
pip install flask
Создайте пустой скрипт, затем скопируйте и вставьте туда следующий код:
xxxxxxxxxx
from 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-запросы. Модифицируем код:
xxxxxxxxxx
from 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" - цена товара.
Отчёт по лабораторной работе оформляется в соответствии с указанными в разделе Правила оценивания требованиями.
В отчёте создайте раздел (заголовок второго уровня) Постановка задачи и продублируйте туда соответствующий блок из этого документа.
Создайте раздел (заголовок второго уровня) Выполнение работы и текстом подробно опишите всё, что делали в процессе выполнения. В описании обязательно должны присутствовать:
В папке с лабораторной работой должно быть: