Azure и Alexa: создаем беседу с легкостью
Как вы знаете, я написал сообщение в блоге некоторое время назад о создании интеграции Microsoft Flow, которая запускает сервер Minecraft в Azure для моих детей. После нескольких месяцев использования этой установки я стал раздражаться тем, сколько раз дети должны были отправить мне сообщение, позвонить мне и т. д., чтобы попросить меня запустить для них сервер. После того, как я некоторое время занимался нашим пространством Bot Framework и теперь сосредоточился на нашей платформе Serverless, мне пришла в голову мысль: почему бы просто не сделать этого бота, который они могут попросить запустить сервер?
Благодаря тому, что мой MS Flow доступен через HTTP-запрос, это становится довольно просто. Просто сконструируйте свой разговор, отправьте его в место, где оно может быть удалено из Bot Framework (Azure App Service и т. д.), и все готово!
Но что, если мы хотим сделать еще один шаг: что, если мы хотим быстро и легко отправить Alexa Skill, который включает сервер в Azure? Оказывается с функциями Azure и отличным пакетом Alexa .NET от моего коллеги Тима Хейера, это невероятно быстро и просто!
Создайте свою функцию Azure
Запустите Visual Studio 2017 и создайте новую Функцию Azure.
Для интеграции Alexa ваша Функция Azure должна быть конечной точкой HTTP (функция v2 отлично работает, поскольку она основана на стандарте .NET):
После того, как ваш проект настроен, вам нужно добавить только один дополнительный пакет nuget:
Install-Package Alexa.NET
Реализуйте протокол квитирования Alexa
Теперь мы готовы к интеграции с Alexa. Если вы когда-либо слышали или делали развитие Alexa Skill, вы знаете, что существует довольно строгий путь аутентификации, который использует Alexa Skill для проверки того, что ваша конечная точка хороша для связи с бэкэндом Alexa Skill и является конечной точкой, которая должна обрабатывать запросы. К счастью, Alexa.NET делает это очень просто.
Измените подпись вашего метода функции, чтобы принять функции POST-запросов, санкционированных функциями (легко вставить параметр строки запроса в конфигурацию Alexa Skill):
public static async Task<IActionResult>
RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req,
TraceWriter log)
Затем очистите содержимое обработчика вашей функции и замените его следующим:
if (!(await IsValid(req)))
{
return new BadRequestResult();
}
return new OkResult();
и добавьте метод IsValid в свой класс Function, который выглядит так:
private static async Task<bool> IsValid(HttpRequest request, TraceWriter log)
{
// Verify SignatureCertChainUrl is present
request.Headers.TryGetValue("SignatureCertChainUrl", out var signatureChainUrl);
if (String.IsNullOrWhiteSpace(signatureChainUrl))
{
log.Error(@"Alexa - empty Signature Cert Chain Url header");
return false;
}
Uri certUrl;
try
{
certUrl = new Uri(signatureChainUrl);
}
catch
{
log.Error($@"Alexa - Unable to put sig chain url in to Uri: {signatureChainUrl}");
return false;
}
// Verify SignatureCertChainUrl is Signature
request.Headers.TryGetValue("Signature", out var signature);
if (String.IsNullOrWhiteSpace(signature))
{
log.Error(@"Alexa - empty Signature header");
return false;
}
// rewind body so we get the full payload
request.Body.Position = 0;
var body = await request.ReadAsStringAsync();
// rewind so caller gets body back at the beginning
request.Body.Position = 0;
if (String.IsNullOrWhiteSpace(body))
{
log.Error(@"Alexa - empty Body");
return false;
}
var valid = await RequestVerification.Verify(signature, certUrl, body);
if (!valid)
{
log.Error(@"Alexa - RequestVerification.Verify failed");
}
return valid;
}
После этого ваш навык теперь будет правильно отвечать на запросы от Alexa и проверять их, когда это необходимо (например: будь то тестирование, бета-тестирование или производство).
Напишите разговор
Теперь давайте перейдем к сути реализации вашего разговора.
Была одна неожиданно интересная вещь, которую я узнал, когда я разрабатывал свой первый навык Alexa: умение никогда не занимает более 8 секунд, чтобы закончить свою очередь, период. Хотя вы можете добавить ответы «прогресса» на то, что отправляет Alexa, это не увеличивает время, которое вы получаете, чтобы «заполнить» запрос у пользователя.
Для меня это означало, что мне нужно было переосмыслить, как я выполняю свою задачу. Сначала я хотел, чтобы Алекса рассказывала моим детям, когда сервер наконец-то начинал работать, или когда завершение выполнено. Задача Azure Automation в приложении Logic имеет параметр «Подождать завершения», но, конечно, эта задача почти никогда не завершается менее чем за 8 секунд.
Это означало, что я должен был изменить то, как я выполнял мои запросы Start/Stop, вместо этого использовать API REST Azure Management - гораздо более быстрый подход. Но я отвлекся.
Посмотрим, как мы пишем разговор в Alexa Skill, используя Alexa.NET.
Первое, что нужно сделать - разобрать полезную нагрузку, которая пришла к вашему навыку, как только вы определили, что все в порядке. Для этого просто возьмите JSON, который был отправлен, и включите его в объект SkillRequest из Alexa.NET следующим образом:
var requestPayload = await req.ReadAsStringAsync();
var alexaRequest = JsonConvert.DeserializeObject<SkillRequest>(requestPayload);
Подобно навыку Cortana, Alexa начинает свое мастерство с «намерения», на который реагирует ваш код. Alexa.NET прекрасно сопоставляет их с сильными типами, поэтому вы можете сделать следующее:
SkillResponse response = null;
try
{
if (alexaRequest.Request is LaunchRequest launch)
{
response = ResponseBuilder.Ask($@"Welcome to the Minecraft bot. Whatchya wanna do.",
new Reprompt { OutputSpeech = new PlainTextOutputSpeech
{ Text = @"Sorry, I don't understand. You can say start the server,
stop the server, or is it on" } });
}
else if (alexaRequest.Request is IntentRequest intent)
Здесь вы видите алгоритм ответа на запрос моих детей «Alexa, запусти Minecraft Bot». Это будет происходить из бэкэнда как намерение LaunchRequest, которым я могу управлять.
Это также знакомит нас с одноразовой природой Alexa. Каждый запрос, который должен быть выполнен к вашему навыку в Alexa.NET долен соответствовать один результат SkillResponse, который должен быть возвращен в конце обработчика. Для меня код становился наиболее простым, когда я установил единую переменную ответа и в нижней части кода моей функции просто возвращал ответ; (вы увидите немного этого с готовым продуктом).
Также, как и Cortana, Alexa отображает то, что говорят ваши пользователи помимо «запуск», «открыть», «старт» и т. п., для определения ваших намерений в портале разработчиков (подробнее об этом позже). Для меня обслуживающие потоки, которые я ожидал, заключались в том, чтобы запустить сервер, остановить его или проверить, включен ли он. Итак, этот код выглядит следующим образом:
if (intent.Intent.Name.Equals(@"is_it_on"))
{
...
}
else if (intent.Intent.Name.Equals(@"start_server"))
{
...
}
else if (intent.Intent.Name.Equals(@"stop_server"))
{
...
}
else
{
...
}
В каждом обработчике для намерения мы просто реализуем работу и ответ на отправку. Вот как выглядит это is_it_on:
var prog = new ProgressiveResponse(alexaRequest);
if (prog.CanSend())
{
await prog.SendSpeech(@"One second, checking.");
}
if (IsVmStarted(_config))
{
response = ResponseBuilder.Tell(@"Yup, it's up and running!");
}
else
{
response = ResponseBuilder.Ask(@"Nope it's off. Would you like to start it?",
new Reprompt { OutputSpeech = new PlainTextOutputSpeech
{ Text = @"Sorry, I don't understand. Just say yes or no" } });
}
Здесь мы также познакомимся с концепцией «прогрессивного ответа». Теперь помните, что даже если вы отправили ответ, когда ваш навык запущен, вы все равно получаете 8 секунд, чтобы установить и вернуть объект ответа, который мы загружаем. Вы можете видеть, что Progressive Response отправляется пользователю сразу же по тому факту, что вы должны ждать метода SendSpeech.
Это также вводит концепцию «Ask» от Alexa. Для моего навыка, если сервер не включен, я позволяю детям узнать и дать им возможность сразу же начать с этой подсказки. Мы поговорим о том, как это будет определено на портале навыков в ближайшее время.
Есть два других намерения, для которых я также реализовал обработку: start_server, где я говорю детям «ОК, Я послал запрос!» И stop_server, где я просто выдаю запрос REST и даю знать, что он отключается.
Обработка ответа на вопрос, иначе «Ask», от Alexa
Я уже упоминал ранее, что, когда дети запрашивают статус сервера, я иду вперед и сообщаю им, что он включен или выключен, и если он выключен, я спрашиваю, хотят ли они, чтобы я включил его. Если они ответят в данном случае «да» я, очевидно, буду запускать эту процедуру и сообщу им, что она включается. Но откуда я знаю, что их ответ («да», «нет») отвечал на вопрос Alexa, а не на какой-то другой?
Введите значение ‘prompt’ (подсказка) в каждом запросе, поступающем от Alexa.
На портале вы можете настроить подсказки для любого намерения (т. е. подсказки, которые возвращаются от Alexa после того, как она обрабатывает конкретное намерение). Для этого у вас есть несколько областей в консоли Alexa Skills Kit:
- Определите тип слота для ожидаемых ответов от вашего пользователя в подсказке.
Здесь вы можете увидеть, что я создал новый тип слота, называемый PROMPTOPTIONS. Я буду использовать это позже. Вот как это выглядит:
Особо обратите внимание на области значений и синонимов. Значение - это то, что вы будете проверять в коде, синонимы используются для обучения базовой модели Natural Language (NL) для каждого из значений, поэтому теоретически мои дети должны иметь возможность сказать «yah», и это должно получить сопоставление с да, хотя я не определил это явно как синоним.
После того, как вы определили свой тип слота, пришло время сообщить намерению is_it_on использовать его сейчас.
- Направьте существующее намерение использовать новый тип слота:
В моем намерении is_it_on я теперь добавляю слот Intent, вызываю его приглашение и сопоставляю его с новым типом слота PROMPTOPTIONS. Кроме того, я добавляю высказывание для моего намерения, в котором говорится, что пользователь может сказать одно из значений слота (например: «да», «нет»), и это должно вызвать это намерение. Это кажется немного проблематичным в моем сознании: я полагаю, что мои дети могут сказать «начинать бот по Minecraft» Alexa, и если она отвечает «Что вы хотите сделать сделать?», они могут просто сказать «да» и в конечном итоге вызвать намерение is_it_on, что было бы нежелательно. Я не пробовал этот поток пока, однако.
После внесения этих двух изменений нажмите «Сохранить модель» и «Создать модель», чтобы новая языковая модель была активной для вашего навыка.
Теперь давайте вернемся к коду:
response = ResponseBuilder.Ask(@"Nope it's off. Would you like to start it?",
new Reprompt { OutputSpeech = new PlainTextOutputSpeech
{ Text = @"Sorry, I don't understand. Just say yes or no" } });
Здесь мы говорим Alexa задать пользователю вопрос. Следующий ответ пользователя будет возвращаться под тем же намерением, с каким было предложено в процессе (например: is_it_on), но с соответствующими слотами. Таким образом, код обработки для моего намерения is_it_on теперь выглядит следующим образом:
if (intent.Intent.Name.Equals(@"is_it_on"))
{
string promptResponse = intent.Intent.Slots[@"prompt"]?.Value;
if (!string.IsNullOrEmpty(promptResponse))
{
if (promptResponse.Equals("yes", StringComparison.OrdinalIgnoreCase))
{
response = StartServer(alexaRequest);
}
else
{
response = ResponseBuilder.Empty();
}
}
else
{
_logger.TrackEvent(@"AlexaIsItOn");
var prog = new ProgressiveResponse(alexaRequest);
if (prog.CanSend())
{
await prog.SendSpeech(@"One second, checking.");
...
Вы теперь видите, я проверяю этот слот, который мы создали, подказку, чтобы узнать, была ли она заполнена чем-либо, и если да, и это значение “да”, продолжайте и включите сервер. В противном случае просто перестаньте отвечать (ResponseBuilder.Empty ()).
Сберегая полную детализацию бота (вызывая API Azure Management для выполнения работы и т. д.),, теперь вы сможете увидеть, как быстро и просто написать Alexa Skill, используя Azure Functions и Alexa.Net!