65.9K
CodeProject 正在变化。 阅读更多。
Home

Azure 和 Alexa,轻松创建对话

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2018 年 5 月 27 日

CPOL

8分钟阅读

viewsIcon

13573

快速了解如何创建由 Azure Functions 支持的 Alexa Skill。

你可能会惊讶地发现,作为一个微软的死忠粉,我居然拥有一个 Alexa 设备。

如你所知,我之前写过一篇博客文章,讲述如何创建一个 Microsoft Flow 集成,在 Azure 中为我的孩子们启动一个 Minecraft 服务器。使用这种设置几个月后,我开始对孩子们不得不给我发消息、打电话等等只是为了让我为他们启动服务器的次数感到恼火。在我们的 Bot Framework 领域工作了一段时间,现在又专注于我们的无服务器平台后,我突然想到——为什么不直接把这变成一个他们可以请求启动服务器的机器人呢??

通过让我的 MS Flow 可以通过 HTTP 请求访问,这变得非常容易。只需设计你的对话,将其部署到 Bot Framework 可以访问的地方(Azure 应用服务等),然后就完成了!

但是,如果我们想更进一步呢?如果我们想快速轻松地部署一个在 Azure 中启动服务器的 Alexa Skill 呢?事实证明,使用 Azure Functions 和我的同事 Tim Heuer一个出色的 Alexa .NET 包,这简直是又快又容易!

创建你的 Azure Function

启动 Visual Studio 2017,创建一个新的 Azure Function。

对于 Alexa 集成,你的 Azure Function 需要是一个 HTTP Endpoint(v2 Function 运行良好,因为它基于 .NET Standard)

项目设置完成后,你只需添加一个额外的 nuget 包

Install-Package Alexa.NET

实现 Alexa 的握手协议

现在我们准备与 Alexa 集成。如果你曾听说过或进行过 Alexa Skill 开发,你会知道 Alexa Skill 会有一个相当严格的身份验证路径,以验证你的端点是否适合与 Alexa Skill 后端通信,并且应该处理请求的端点。幸运的是,Alexa.NET 使这变得轻而易举!

修改你的函数方法的签名以接受 Function 授权的 POST 请求(将查询 string 参数嵌入到你的 Alexa Skill 配置中很容易)

public static async Task<IActionResult> 
RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, 
TraceWriter log)

接下来,清空你的 Function 处理程序的内容,并将其替换为以下内容

if (!(await IsValid(req)))
{
    return new BadRequestResult();
}

return new OkResult();

并在你的 Function 类中添加一个 IsValid 方法,如下所示

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;
}

完成此操作后,你的 Skill 将正确响应来自 Alexa 的请求,并在适当的时候对其进行验证(例如:在测试、Beta 版或生产环境中)。

编写对话

现在让我们来看看实现对话的核心部分。

在开发我的第一个 Alexa Skill 时,我学到了一件我没有预料到的有趣的事情:一个 Skill 永远不能花费超过 8 秒来结束它的回合。虽然你可以为 Alexa 发送回来的内容添加“进度”响应,但这些并不能延长你“完成”用户请求的时间。

对我来说,这意味着我需要重新思考我执行任务的方式。最初,我希望 Alexa 在服务器最终启动并运行或关闭完成时告知我的孩子们。Logic App 中的 Azure 自动化任务具有“等待完成”选项,但当然,这个任务几乎从未在 8 秒内完成。

这意味着我不得不改变我执行启动/停止请求的方式,转而使用 Azure 管理 REST API;这是一种更快的方法。但我跑题了。

让我们看看如何使用 Alexa.NET 在 Alexa Skill 中编写对话。

首先要做的是在确定一切正常后解析传入 Skill 的有效负载。为此,只需获取发布的 JSON 并将其转换为 Alexa.NET 的 SkillRequest 对象,如下所示

var requestPayload = await req.ReadAsStringAsync();
var alexaRequest = JsonConvert.DeserializeObject<SkillRequest>(requestPayload);

就像 Cortana Skill 一样,Alexa 会以一个“Intent”启动你的 Skill,你的代码会对此做出反应。Alexa.NET 将这些 nicely 映射到强类型,因此你可以执行以下操作

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 的单回合特性。对你的 Skill 的每个请求都必须在 Alexa.NET 中以单个 SkillResponse 结果来满足,该结果必须在处理程序结束时返回。对我来说,如果我设置一个单一的 response 变量,并在我的 Function 代码的底部简单地 return response;,代码会变得最容易(你稍后会在成品中看到这一点)。

同样,与 Cortana 非常相似,Alexa 将你的用户所说的内容(除了“launch”、“open”、“start”之类的词)映射到你在开发者门户中定义的意图(稍后会详细介绍)。对我来说,我预期要处理的流程是启动服务器、停止服务器或检查它是否开启。所以这段代码看起来像这样

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" } });
}

在这里,我们还引入了“渐进响应”的概念。现在请记住,即使你的 Skill 正在运行时已经发回了一个响应,你仍然只有 8 秒来设置并返回我们正在填充的 response 对象。你可以看到渐进响应是通过你必须 await SendSpeech 方法而立即发送给用户的。

这也引入了 Alexa 的“Ask”概念。对于我的 Skill,如果服务器没有开启,我就会告知孩子们并给他们选择立即启动的提示。我们很快就会更详细地介绍这如何在 Skill 门户中定义。

我还实现了另外两个意图的处理:start_server,我告诉孩子们“好的,我已发送请求!”;以及 stop_server,我只是发出 REST 请求并让他们知道服务器正在关闭。

处理 Alexa 问题的答案,也就是“Ask”

我之前提到过,当孩子们询问服务器状态时,我会告诉他们服务器是开还是关,如果关了,我会问他们是否要我打开。如果他们回答“是”,显然我希望运行该例程并让他们知道服务器正在开启。但是我怎么知道他们的回答(“是”、“否”)是回复 Alexa 的提问,而不是其他什么呢?

每个来自 Alexa 的请求中都包含“prompt”值。

在门户中,你可以为任何意图配置提示(即,Alexa 处理完特定意图后返回的提示)。为此,你需要在 Alexa Skills Kit 控制台中填写几个区域

  1. 为用户对你提示的预期响应定义一个槽位类型。

    在这里,你可以看到我创建了一个名为 PROMPTOPTIONS 的新槽位类型。我稍后会使用它。它看起来像这样

    请特别注意 valuesynonyms 区域。value 是你将在代码中检查的内容,synonyms 用于训练每个值的底层自然语言 (NL) 模型,因此理论上,我的孩子们应该能够说“yah”,它应该被映射到 yes,即使我没有明确将其定义为同义词。

    定义了槽位类型后,现在是时候告诉 is_it_on 意图使用它了。

  2. 指示现有意图使用新的槽位类型

    在我的 is_it_on 意图中,我现在添加了一个意图槽位,称之为 prompt 并将其映射到新的 PROMPTOPTIONS 槽位类型。此外,我为我的意图添加了一个话语,说明用户可以说明其中一个槽位值(例如:“是”、“否”),它应该触发此意图。这在我看来有点问题;我感觉我的孩子们可以对 Alexa 说“启动我的世界机器人”,当她回答“你想做什么?”时,他们可以直接说“是”,最终触发 is_it_on 意图,这可能不是我们想要的;不过,我还没有尝试过这个流程。

完成这两项更改后,单击“保存模型”和“构建模型”以使新语言模型对你的 Skill 生效。

现在让我们回到代码

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.");
...

现在你可以看到我正在检查我们创建的槽位 prompt,看看它是否填充了任何内容,如果填充了并且是 yes 值,则继续打开服务器。否则,只需停止响应 (ResponseBuilder.Empty())。

省略了机器人的全部细节(调用 Azure 管理 API 来完成工作等),希望你现在能看到使用 Azure Functions 和 Alexa.Net 编写 Alexa Skill 是多么快速和容易!好奇这最终可能会花费你多少钱?我想你可能会非常惊讶

© . All rights reserved.