.NET on AWS Blog

Building an Alexa Skill with AWS Lambda and Amazon DynamoDB – Part 2: Creating the Skill to Query Data

This is Part 2 in a series on creating an Amazon Alexa Skill with AWS Lambda and Amazon DynamoDB. Please look at Part 1: Creating Data for the Skill to see the prerequisites on setting up the data source for our skill. In this part, the Alexa Skill we will build will return information about a prompted movie. At the end of this post, we will have a fully functional Alexa Skill that leverages AWS Lambda functions to return that movie data.

Configuring the AWS Alexa skill

Before you build the backend service for the skill, open the Alexa Developer Console and create the skill.

Alexa Developer Console Skills Section Empty textbox to provide skill name and "Create Skill" button

You must provide a skill name (not the command to start it), select Custom Model, and then select Provision your own backend.

Create a new skill wizard in Alexa Developer Console. Skill name value is "Check TMDB". In "1. Choose a model to add to your skill" section, Custom option is selected.

To trigger the newly created skill from Alexa you must configure a Skills Invocation Name.

In Custom Skill Configuration in Alexa Developer Console. Invocations option is expanded, Skill Invocation Name configuration page open. Skill Invocation Name is populated with "check t. m. d. b."

One thing to remember is that the Invocation Name displays as “check t. m. d. b.”. The reason for this is that Alexa takes natural speech as input, so it forms requests similar to Talk to Text.

Alexa uses Intents as ways to navigate outcomes for requests. For instance, the invocation would be treated by Alexa as a LaunchRequest (more on this later). For our purpose, you should create a new intent called GetMovieInfo, this intent will be triggered when someone says a movie title. But how will Alexa know it is a movie title? Because Alexa uses the concept of Slot Types which act as variables for intent. There are a ton of Slot Types provided (Cities, Sports, and in our case, Movies) or you can create a custom list of values of a slot. Since you can use the provided Movie slot, just identity a few sample utterances. After you create a Slot Type you must Build your Model and tools. The final configuration should look like the following example.

Intents section of Alexa Developer Console Intent name is GetMovieInfo

Alexa skills provide a wide range of extensive customization options. If you are curious to learn more, be sure to check out all the documentation for Alexa Custom Skills.

You can test the skill in any browser, to make sure the intents are working as expected with the Utterance Profiler. You should check the correct movie title was passed and it triggered the correct intent.

Utterance Profiler of Alexa Developer Console Provided utterance value is "Jurassic World Dominion". Selected intent is GetMovieInfo with Slot Value of "Movie: Jurassic World: Dominion"

After testing to ensure the correct movie titled was passed go back to Visual Studio and configure the backend service for the skill.

Writing the skill as an AWS Lambda function

The role of your other Lambda function is to serve as a backend for our Alexa Skill, but the setup is similar to that of the seed data. You can create the project and configure the dependency injection the same way. Once completed, you can continue to build the logic that scans the DynamoDB table and the FunctionHandler to interact with the requests sent by Alexa.

Scanning the table

When someone calls the Alexa Skill, they are going to supply a movie, and the skill will respond with that movie’s release date, for instance:

Me: “Alexa, check t. m. d. b.”
Alexa: “Ok, provide a movie”
Me: “Jurassic World Dominion”

Alexa: “Jurassic World Dominion was released on June 10th, 2022”

Remember that you are saving only a handful of properties in your datastore related to the movies, and when the table is scanned, you want to search for the movie that has the title provided from Alexa. Alexa sends a request to our FunctionHandler, which is than used to call the DynamoDBService.

public async Task<MovieModel> SearchTable(string searchText)
{
    DynamoDBContext context = new DynamoDBContext(_client); 
    var conditions = new List<ScanCondition>()
    {
    new ScanCondition("TitleLower", Amazon.DynamoDBv2.DocumentModel.ScanOperator.Contains, searchText.ToLower())
    }; 
    var queryResult = await context.ScanAsync<MovieModel>(conditions).GetRemainingAsync(); 
    if (queryResult.Count > 0)
    {
        return queryResult.FirstOrDefault();
    }
    return null;
}

The important part of this code is that there is a property in the table called title_lower which is the lower-case representation of the movie title. You than take the value provided from Alexa, make it lower-case, and then search the title_lower field where the value contains the text from Alexa. If you get any values back, build a temporary Movie object and send that back to the FunctionHandler. This method isn’t perfect as fuzzy search and relevance is hard to build, but in this case for a demo, it will work fine. One interesting option to level-up this demo is to use a different AWS service like Amazon Kendra or Amazon OpenSearch Service which can provide an improved search experience. With the scan method done, you can move to building the interaction with Alexa.

Handling Alexa Skill Intents

When Alexa makes a request to our backend service, the request is in a specific format based on the configured intents. In order to interact effectively with Alexa, you can use the Alexa.NET library available on NuGet (code is also on GitHub, YAAAY Open Source!).

The basis of Alexa.NET is that it handles the SkillRequest sent from Alexa and unwraps the request to identify the type of request. If it is a LaunchRequest (check t. m. d. b.), it will prompt the user for a movie. If it is an IntentRequest, you can identify the type of intent and check for your GetMovieInfo intent, which when found will get the value from the Movie slot in the IntentRequest. Then take that slot value and pass it to search the DynamoDB table.

public async Task<SkillResponse> Handler(SkillRequest input)
{            
    Type requestType = input.GetRequestType();
    if (input.GetRequestType() == typeof(LaunchRequest))
    {
        string speech = "Please provide a movie you would like to know more about";
        Reprompt rp = new Reprompt("Provide a movie");
        return ResponseBuilder.Ask(speech, rp);
    }
    else if (input.GetRequestType() == typeof(SessionEndedRequest))
    {
        return ResponseBuilder.Tell("Thanks for using the TMDB Skill!");
    }
    else if (input.GetRequestType() == typeof(IntentRequest))
    {
        var intentRequest = (IntentRequest)input.Request;
        switch (intentRequest.Intent.Name)
        {
            case "AMAZON.CancelIntent":
            case "AMAZON.StopIntent":
                return ResponseBuilder.Tell("Thanks for using the TMDB Skill!");
            case "AMAZON.HelpIntent":
                {
                    Reprompt rp = new Reprompt("What's next?");
                    return ResponseBuilder.Ask("Here's some help. What's next?", rp);
                }
            case "GetMovieInfo":
                {
                    var movie = await _dbService.SearchTable(intentRequest.Intent.Slots["Movie"].Value);
 
                    if (movie != null)
                    {
                        return ResponseBuilder.Tell($"{movie.Title} was released on {DateTime.Parse(movie.ReleaseDate).ToShortDateString()}
");
                    }
                    return ResponseBuilder.Tell($"No movies found, please try again");
                }
            default:
                {
                    _logger.LogInformation($"Unknown intent: " + intentRequest.Intent.Name);
                    string speech = "I didn't understand - try again?";
                    Reprompt rp = new Reprompt(speech);
                    return ResponseBuilder.Ask(speech, rp);
                }
        }
    }
    return ResponseBuilder.Tell("Thanks for using the TMDB Skill!");
}

This code lives in your LambdaEntryPoint (similar to the one created for the SeedData project). When each piece is completed, you can test the experience using the Lambda Mock Test Tool, which has sample Alexa Requests. Here is a sample request that will work for your skill.

{
  "version": "1.0",
  "session": {
    "new": false,
    "sessionId": "amzn1.echo-api.session.[unique-value-here]",
    "application": {
      "applicationId": "amzn1.ask.skill.[unique-value-here]"
    },
    "attributes": {},
    "user": {
      "userId": "amzn1.ask.account.[unique-value-here]"
    }
  },
  "context": {
    "AudioPlayer": {
      "playerActivity": "IDLE"
    },
    "System": {
      "application": {
        "applicationId": "amzn1.ask.skill.[unique-value-here]"
      },
      "user": {
        "userId": "amzn1.ask.account.[unique-value-here]"
      },
      "device": {
        "supportedInterfaces": {
          "AudioPlayer": {}
        }
      }
    }
  },
  "request": {
    "type": "IntentRequest",
    "requestId": "amzn1.echo-api.request.[unique-value-here]",
    "timestamp": "2016-10-27T21:06:28Z",
    "locale": "en-US",
    "intent": {
      "name": "GetMovieInfo",
      "slots": {
        "Movie": {
          "name": "Item",
          "value": "Jurassic World Dominion"
        }
      }
    }
  }
}

If you execute the request, you should see a successful response.

Response and Log Output section of AWS .NET Core 6.0 Mock Lambda Test Tool showing successful Response value {"version":"1.0","response":{"outputSpeech":{"type":"PlainText","text":"Jurassic World Dominion was released on 6/1/2022"},"shouldEndSession":true}}

Deploy skill to AWS

Now you are ready to deploy the skill to AWS and finish up by pointing the Alexa Skill at the endpoint. To do this, you are going to deploy via Visual Studio again, the only difference this time is you give it a new function name.

Upload Lambda Function Dialog in Visual Studio 2022 with function name populated with "TMDBAlexaSkill"

Create a new role and Upload. Once complete, you will need to add a read-only role to the Lambda function so follow similar steps in part 1, instead this time select AmazonDynamoDBReadOnlyAccess and add that policy to the Lambda function role like before.

Attach policy page for ""lambda_exec_TMDBAlexaSkill" in AWS Console. Policy name "AmazonDynamoDBFullAccess" highlighted

The last step is to point the Alexa Skill to the Lambda function. You can do this by adding an Alexa trigger to the newly deployed function. From here, it will ask for a Skill ID, which you can get from the Alexa Developer Console in the Endpoint section of the skill. Enter that ID into the text box and choose Add. Finally, take the Function ARN and copy that into the Default Region of the Service Endpoint in the Alexa Console.

Endpoint Configuration screen of Alexa Developer Console. Service Endpoint Type radio button selected "AWS Lambda ARN". Default Region text box is populated with Function ARN from AWS Console

When all said and done, the function overview should look like the following.

You can end to end test in the Alexa Developer Console, by going to the Test tab, and set the Skill testing is enabled in: value to Development and simulating like so.

Sample interaction with skill in Alexa Developer Console. check t. m. d. b. Please provide a movie you would like to know more about jurassic world dominion Jurassic Would Dominion was released on 6/1/2022

And finally, you can also test on an actual Alexa device if you have that device on the same account as the Alexa Developer account. That would look similar to:

After following the steps in the 2 blog posts, you have a skill powered by an AWS Lambda function that uses a datastore.

Resource Cleanup

To ensure no additional costs of walking through this tutorial, it would be best to delete your Lambda functions and DynamoDB table. The fastest way to do this is via the AWS Command Line Interface (CLI).

aws lambda delete-function --function-name name-of-seed-function

aws lambda delete-function --function-name name-of-skill-function

aws dynamodb delete-table --table-name movies

What’s next?

AWS Lambda Functions are extremely valuable and we hope it inspires you to build .NET applications on AWS. This project would not be possible without the great Open Source projects:

Conclusion

Now we have an Alexa Skill that calls an AWS Lambda function to return movie information. To do this, we created an AWS Lambda Function and DynamoDB table in Part 1 that holds our movie information. In this post, we created another AWS Lambda function that queries the data in the table. Finally, we created an Alexa Skill that takes user prompt and executes that Lambda, ultimately returning the data.