Shadministration — Byte-sized adventures in tech

Querying DynamoDB using Lambda (Python)

Let me start by saying that I don’t really use Alexa or Echo devices. I do think they’re cool, I just don’t see many practical applications for voice-assistants. One case where I do think a voice assistant could be helpful though is for searching or modifying data in a database using a conversational input. One idea I had to test this was to make a simple product location lookup app for Alexa. Let’s say you’re in a grocery store and you’re looking for a specific product (and you don’t know what isle it’s in). You have a couple of options: A) visually scan all the signs above each isle; B) look up the product in an alphabetically-sorted paper list of products with their corresponding isle at the end of the nearest isle (if available); or C) ask an associate. Imagine if there was an Amazon Echo or Alexa-enabled device where you could ask Alexa where a specific product is, have her look it up in a database and then tell you what isle it was in.

You know what I’m talking about… where’s the marshmallows?!

Now, the grocery store could use a mobile app for this but customers would have to download it. They could also use a tablet-like kiosk (I’ve actually seen these in Target), but those would likely be more expensive and it would take longer to punch in the item you are searching for. Additionally, in recent days with Covid-19, it would be an added bonus if people didn’t have to physically touch a shared device. My real motivation with this project was to get more experience with AWS and Python, but you could see how this could be a potential use-case.

So the architecture for this is pretty simple. I started with a sample skill template from AWS written in Python which I modified to detect the “slot”, or product that the user includes in their verbal search request. It then searches for the slot variable in a pre-constructed DynamoDB table containing all the products and corresponding isles and returns the isle to the user. At a high level, the individual steps I did for this project were as follows:

  1. Create the new skill in the Alexa Developer Console and configure intents, slots, skill name, etc.
  2. Create a table in DynamoDB and enter the data.
  3. Create an IAM role with permission to query DynamoDB and assign it to the skill.
  4. Edit the lambda function code in the Alexa skill to make a connection to the database and return an isle value based on the given product.

Step one is pretty straight-forward. You start by opening the Alexa Developer Console and creating an invocation name. You then create a “slot”, an “intent”, and define “utterances”. Your sample utterances are phrases or combinations of words that will trigger your skill. You can think of a slot as the variable in your utterance. For example, in my “Isle Finder” skill, I wanted the user to be able to say “Alexa, ask Isle Finder where [product(s)] is/are…” in order to trigger the skill. So under “Slot types” in the Alexa Developer Console menu, I added a slot called “Product” and entered all the possible values for that slot (i.e. bread, milk, honey, etc). For my utterances, I added “where {Product} is” and “where {Product} are”, where “{Product}” references the Product slot I defined previously.

My “Product” slot and some values (the ones at the top are auto-generated from a built in list).
Sample utterances to trigger the “isleFinderIntent” intent using the “Product” slot.

Once that was complete, I moved on to entering product data in DynamoDB. Again, creating the table and entering data is pretty straightforward. One speed-bump I encountered however was querying the database using a value OTHER than the primary key (ProductID). In DynamoDB, you need something called a Global Secondary Index in order to query a table based on an index other than your primary key index. Now, in my table I could have set ProductName as the primary key without compromising on functionality, but in the real world the primary key in a table is usually an ID number or similar.

Thankfully, creating and querying a GSI is pretty easy. Inside your DynamoDB table in the AWS console, click the “Indexes” tab, click the “Create Index” button, and then enter in the partition key, index name, and your desired read/write capacity units. The partition key is the name of the key you want to query on. In my case I used “ProductName”. The index name is just that – just come up with an index name for the GSI (i.e “ProductNameindex”). For the capacity units, I changed these to 1 for both read and write to reduce cost (and because that’s all I need). This is the amount of read or write activity the table can support measured in strongly-consistent reads per second. You can read more about this here. Luckily, AWS offers 25 provisioned read-capacity units and 25 write-capacity units free forever, so I didn’t incur any additional charges.

My “Product_List” DynamoDB table.
Global Secondary Index configuration.

The Code…

With all of our supporting pieces in hand, let’s take a look at how to put them together with the code in our Lambda function. There’s a few steps we need to do in order for this to work at a basic level:

  1. Change our intent name in the “HelloWorldIntentHandler” function.
  2. Import a few dependencies.
  3. Get the “slot” (“Product”) and put it in a variable.
  4. Make the skill assume the role we created earlier (to read our table).
  5. Query the table on the GSI for “Product_Name” we created earlier and return the corresponding isle value.

Starting with the “HelloWorld” skill code, I changed in the “HelloWorldIntentHandler” function in order to call my renamed Intent. I simply replaced “HelloWorldIntent” with the name of my intent.

class isleFinderIntentHandler(AbstractRequestHandler):
    """Handler for Hello World Intent."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return ask_utils.is_intent_name("isleFinderIntent")(handler_input)

In order to perform operations on DynamoDB, I needed to import the boto3 AWS SDK for Python.

import boto3
from boto3.dynamodb.conditions import Key

Now for the meat and potatoes (pun intended)… The following code in the “Handle” function of the “HelloWorldIntentHandler” class retrieves the slot (what product the user is inquiring about) from the spoken request and sets it to a variable named “product_slot”. We’ll reference that variable later in our response:

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        slot = ask_utils.request_util.get_slot(handler_input, "Product")
        product_slot = ("{}".format(slot.value))

The following code is used to assume the AWS role that will allow this program to query our DynamoDB table. I more or less copied this from this AWS knowledge base article. The only thing I had to change was the Role ARN and the region accordingly.

    # ------- Role Assumption in order to query DynamoDB Product_List table ------- #
    # 1. Assume the AWS resource role using STS AssumeRole Action
    sts_client = boto3.client('sts')
    assumed_role_object=sts_client.assume_role(RoleArn="arn:aws:iam::123456789000:role/LambdaDynamoDBReadOnly", RoleSessionName="AssumeRoleSession1")
    credentials=assumed_role_object['Credentials']

    # 2. Make a new DynamoDB instance with the assumed role credentials
    dynamodb = boto3.resource('dynamodb', aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'], region_name='us-east-1')
    # --------------- End Role Assumption --------------- #

    table = dynamodb.Table('Product_List')

Once we have the table loaded in, the remaining logic to get this working at a basic level is fairly simple.

        response = table.query(IndexName="ProductName-index",KeyConditionExpression=Key('ProductName').eq(product_slot))
        
        response_count = response['Count']
        
        # Make sure only one (1) item is returned from the DynamoDB query. Otherwise, fail.
        if (response_count == 1):
	        [result] = response['Items']
	        # If name of product (slot) does not end in "s", return "is in isle...". Else, return "are in isle..."
	        if (product_slot[len(product_slot) -1] != "s"):
	            speak_output = product_slot + " is in isle " + str(result['Isle'])
	        else: 
	            speak_output = product_slot + " are in isle " + str(result['Isle'])
        else:
            speak_output = "Sorry, I don't know where that product is."

Here’s a link to the most up-to-date version of the complete lambda_function.py for this skill on GitHub Gist. This was a pretty fun project and it has me thinking about what other kinds of Skills I could create that would be useful in the real world. I look forward to publishing a skill in the future (which I’ll be sure to write another post on!).

Leave a Reply

Your email address will not be published.