Alexa Skills mit Azure Functions

Vor einiger Zeit habe ich ja bereits auf eine Möglichkeit hingewiesen, wie man seinen Alexa Skill für eine Azure Function verifizieren kann. Dies ist eine Grundvoraussetzung für die Veröffentlichung des Skills im Skill Store von Amazon. In diesem Beitrag möchte ich euch nun ein paar Beispiele von möglichen Skills aufzeigen, welche man mit der Hilfe von Azure Function schreiben kann.

Ich habe mich für vier einfache Beispiele entschieden, welche jedoch den Grundgedanken ziemlich gut widerspiegeln, so dass der eigenen Entwicklung im Anschluss nichts mehr im Wege steht. Als Basis dient jeweils ein Visual Studio Projekt mit dem Template Azure Functions. Ebenso müssen die NuGet-Packages Alexa.NET und Alexa.NET.Security.Functions installiert sein.

Hello World

Wie es sich für jeden guten Entwickler gehört, werden auch wir erst einmal einen kleinen Hello World Skill entwickeln. Dieser Skill deserialisiert zunächst den Body des Requests als SkillRequest und validiert anschließend diesen, dass es sich wirklich um einen gültigen Alexa-Request handelt.  Anschließend wird einfach eine statische Antwort zurückgegeben. Somit handelt es sich hierbei um ein sehr einfaches Beispiel, welches aber den generellen Aufbau eines Skills verdeutlichen soll.

[FunctionName("AlexaHelloWorldFunction")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "alexa/helloworld")]
    HttpRequest req, 
    ILogger log)
{
    log.LogInformation("AlexaHelloWorldFunction - Started");

    // Get request body
    var content = await req.ReadAsStringAsync();

    // Deserialize object to SkillRequest
    var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(content);

    // Validate SkillRequest
    var isValid = await skillRequest.ValidateRequestAsync(req, log);
    if (!isValid)
        return new BadRequestResult();

    // Return SkillResponse
    return new OkObjectResult(ResponseBuilder
        .TellWithCard("Hello World from an Azure Function!", 
        "Hello World", "Hello World from an Azure Function!"));
}

Hello Name

Der nächste Skill baut auf dem Hello World Beispiel auf. Dieses Mal reagieren wir aber bereits auf das gesagte vom Nutzer. Ziel ist es, dass der Nutzer einen Namen angibt und anschließend begrüßt unser Skill diese Person. Hier gibt es nun zwei Möglichkeiten: Entweder startet der Nutzer zunächst unseren Skill und wir müssen aktiv nach einem Namen fragen oder aber der Nutzer gibt direkt einen Namen an und so können wir direkt die jeweilige Person begrüßen. Dafür müssen wir zwischen dem LaunchRequest und dem IntentRequest unterscheiden und entsprechend andere Werte zurückgeben.

[FunctionName("AlexaHelloNameFunction")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "alexa/helloname")]
	HttpRequest req,
	ILogger log)
{
    log.LogInformation("AlexaHelloNameFunction - Started");

    // Get request body
    var content = await req.ReadAsStringAsync();

    // Deserialize object to SkillRequest
    var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(content);

    // Validate SkillRequest
    var isValid = await skillRequest.ValidateRequestAsync(req, log);
    if (!isValid)
        return new BadRequestResult();

    // Check for launchRequest
    if (skillRequest.Request is LaunchRequest)
        return new OkObjectResult(ResponseBuilder
            .AskWithCard("Welcome to Hello Name! " +
                "Just give me the name of the person I " +
                "should welcome today.", "Hello Name", 
                "Welcome to Hello Name!", 
                new Reprompt("What is your name?")));

    // get name from body data
    var intentRequest = (IntentRequest)skillRequest.Request;
    var name = intentRequest.Intent.Slots.ContainsKey("name") 
        ? intentRequest.Intent.Slots["name"].Value 
        : null;

    if (name == null)
    {
        log.LogInformation("AlexaHelloNameFunction - No name detected");
        return new OkObjectResult(ResponseBuilder
            .TellWithCard("Unfortunately, I did not understand " + 
                "your name correctly...", "Hello Name!", 
                "Unfortunately, your name was not recognized..."));
    }

    log.LogInformation($"AlexaHelloNameFunction - Name: {name}");
    return new OkObjectResult(ResponseBuilder
        .TellWithCard($"How are you, {name.ToUpper()}? " + 
            "I am pleased to meet you.", "Hello Name!", 
            $"Hello {name.ToUpper()}!"));
}

Calculator

Im letzten Beispiel haben wir bereits gesehen, wie wir sowohl mit dem LaunchRequest als auch dem IntentRequest mit einem Slot interagieren können. Nun treiben wir dieses Beispiel etwas weiter voran und entwickeln einen kleinen Taschenrechner, welcher die vier Grundrechenarten für jeweils zwei Zahlen unterstützt. Beim Dividieren überprüfen wir ebenfalls, dass die zweite Zahl nicht 0 ist, da diese Operation mathematisch sonst nicht möglich ist. Der Code ist zwar etwas länger, aber lässt sich eigentlich trotzdem recht einfach verstehen, da zunächst die beiden Zahlen extrahiert werden und anschließend das Ergebnis berechnet wird, bevor es entsprechend an den Nutzer zurückgegeben wird.

[FunctionName("AlexaCalculatorFunction")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "alexa/calculator")]
    HttpRequest req, 
    ILogger log)
{
    log.LogInformation("AlexaCalculatorFunction - Started");

    // Get request body
    var content = await req.ReadAsStringAsync();

    // Deserialize object to SkillRequest
    var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(content);

    // Validate SkillRequest
    var isValid = await skillRequest.ValidateRequestAsync(req, log);
    if (!isValid)
        return new BadRequestResult();

    // check the request type for Launch Request
    if (skillRequest.Request is LaunchRequest)
    {
        // default launch request
        log.LogInformation("AlexaCalculatorFunction - LaunchRequest");
        return new OkObjectResult(HandleHelpRequest());
    }

    // check the request type for Intent Request
    if (skillRequest.Request is IntentRequest)
    {
        var intent = ((IntentRequest)skillRequest.Request).Intent;

        log.LogInformation($"AlexaCalculatorFunction - IntentRequest: {intent}");

        if (!intent.Slots.ContainsKey("firstnum")
		    || !intent.Slots.ContainsKey("secondnum"))
            return new OkObjectResult(ResponseBuilder
                .TellWithCard("Please specify two numbers to be added, " + 
                     "subtracted, multiplied or divided.", "Alexa Calculator", 
                     "Unfortunately, no numbers were given. Please try again " +
                     "with a math task."));

        var num1 = Convert.ToDouble(intent.Slots["firstnum"].Value);
        var num2 = Convert.ToDouble(intent.Slots["secondnum"].Value);
        double result;

        switch (intent.Name)
        {
            case "AddIntent":
                result = num1 + num2;
                return new OkObjectResult(ResponseBuilder
                    .TellWithCard($"The result of adding {num1} and {num2} is: " +
                        "{result}.", "Alexa Calculator", 
                        $"{num1} + {num2} = {result}."));
            case "SubstractIntent":
                result = num1 - num2;
                return new OkObjectResult(ResponseBuilder
                    .TellWithCard($"The result of subtracting {num1} and {num2} is: " +
                        "{result}.", "Alexa Calculator", 
                        $"{num1} - {num2} = {result}."));
            case "MultiplyIntent":
                result = num1 * num2;
                return new OkObjectResult(ResponseBuilder
                    .TellWithCard($"The result of multiplying {num1} and {num2} is: " +
                        "{result}.", "Alexa Calculator", 
                        $"{num1} * {num2} = {result}."));
            case "DivideIntent":
                if (num2 == 0)
                {
                    return new OkObjectResult(ResponseBuilder
                        .TellWithCard("You have just tried to divide by 0. " +
                            "This does not work. Please try with a different task.", 
                            "Alexa Calculator", "You have just tried to divide by 0. " + 
                            "This does not work. Please try with a different task."));
                }
                else
                {
                    result = num1 / num2;
                    return new OkObjectResult(ResponseBuilder
                        .TellWithCard($"The result of dividing {num1} and {num2} is: " +
                            "{result:F2}.", "Alexa Calculator", 
                            $"{num1} / {num2} = {result:F2}."));
                }
            default:
                return new OkObjectResult(HandleHelpRequest());
        }
    }

    return new OkObjectResult(HandleHelpRequest());
}

private static SkillResponse HandleHelpRequest()
{
    return ResponseBuilder.AskWithCard("Welcome to the Alexa Calculator. " + 
        "I can add, subtract, multiply, and even divide two numbers. " + 
        "For example, what is three plus two?", "Alexa Calculator", 
        "Welcome to the Alexa calculator. I can add, subtract, multiply, " + 
        "and even divide two numbers. For example, what is 3 + 2?", 
        new Reprompt("What is your mathematical task?"));
}

Quote

Die bisherigen Beispiele dienten eigentlich nur zur Verdeutlichung, wie man einen eigenen Alexa Skill mit der Hilfe von .NET und Azure Functions schreiben kann, aber werden so ihren Wegen in den Skill Store nicht schaffen. Als letzten Beispiel folgt nun ein Beispiel, welches so auch im Skill Store zur Verfügung steht. Der folgende Skill liefert uns jedes Mal ein zufällige Zitat zusammen mit dem Autor. Hierfür kommt ein externen Dienst zum Einsatz, welcher alle Informationen als JSON-Antwort zur Verfügung stellt und von uns direkt verwendet werden kann.

static class Statics
{
    public static string QuoteUrl 
        = "https://api.forismatic.com/api/1.0/?method=getQuote&format=json&lang=en";
}

class Quote
{
    [JsonProperty("quoteText")]
    public string QuoteText { get; set; }

    [JsonProperty("quoteAuthor")]
    public string QuoteAuthor { get; set; }
}

[FunctionName("AlexaQuoteFunction")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "alexa/quote")]
    HttpRequest req, 
    ILogger log)
    {
        log.LogInformation("AlexaQuoteFunction - Started");

        // Get request body
        var content = await req.ReadAsStringAsync();

        // Deserialize object to SkillRequest
        var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(content);

        // Validate SkillRequest
        var isValid = await skillRequest.ValidateRequestAsync(req, log);
        if (!isValid)
            return new BadRequestResult();

        // Check for launchRequest            
        if (skillRequest.Request is LaunchRequest)
            return new OkObjectResult(ResponseBuilder
                .AskWithCard("Welcome to Random Quote! Everytime you start me " + 
                    "and ask for a random quote, I will give it to you..", 
                    "Random Quote", "Welcome to Random Quote! Everytime you start me " + 
                    "and ask for a random quote, I will give it to you...", 
                    new Reprompt("Give me a random quote")));

        // Check for IntentRequest
        if (skillRequest.Request is IntentRequest intentRequest)
        {
            switch (intentRequest.Intent.Name)
            {
                case "AMAZON.StopIntent":
                case "AMAZON.CancelIntent":
                    return new OkObjectResult(ResponseBuilder
                        .TellWithCard("Ok", "Random Quote", "Till next time."));
                case "AMAZON.HelpIntent":
                    return new OkObjectResult(ResponseBuilder
                        .AskWithCard("Everytime you ask for a random quote, " + 
                            "I will tell you one.", "Random Quote", 
                            "Everytime you ask for a random quote, I will tell you one.",
                            new Reprompt("Give me a random quote")));
                case "RandomQuoteIntent":
                    var quoteString = 
                        await new HttpClient().GetStringAsync(Statics.QuoteUrl);
                    var quote = JsonConvert.DeserializeObject<Quote>(quoteString);
                    return new OkObjectResult(ResponseBuilder
                        .TellWithCard($"{quote?.QuoteText?.Trim()} - {quote?.QuoteAuthor}",
                            "Random Quote", 
                            $"{quote?.QuoteText?.Trim()} - {quote?.QuoteAuthor}"));
            }
        }

        return new OkObjectResult(ResponseBuilder
            .TellWithCard("Something went wrong... Please try again.", 
            "Random Quote", "Something went wrong..."));
    }
}

Ich hoffe, dass diese Beispiele aufzeigen konnten, dass das Verwenden von Azure Function im Zusammenspiel mit einem Alexa Skill relativ einfach ist und man so schnell seinen eigenen Skill schreiben kann. Alle Beispiele befinden sich auch noch auf GitHub zum direkten Ausprobieren.

#Hackschool – Alexa Skill Entwicklung echosim.io – Alexa im Browser Überprüfen, ob ein Echo-Device über ein Display verfügt