Hands on

Geocode Addresses with Amazon Alexa, Golang, and the HERE Geocoder API

By Nic Raboy | 15 November 2018

I’m a big advocate of the Amazon Echo line and Alexa. I have an Echo in pretty much every room of my house and have even published a Skill in the past called BART Control which gives information on the Bay Area Rapid Transit system. Most skills, not all, make use of AWS Lambda and Functions as a Service (FaaS) to operate. Essentially, you create a function, it executes some code, and it returns a response. In Amazon Alexa lingo, you give it a request which is your voice command, it executes some code, and it gives a response which is a voice response.

With the concept of Lambda in mind, we can create a function that does pretty much anything, including working with some of the HERE APIs.

We’re going to explore creating our own Amazon Alexa Skill using a Lambda function written in Golang that takes an address in the command and returns a latitude and longitude coordinate generated by the HERE Geocoder API.

Build an AWS Lambda Function with the Go Programming Language

As of recently, AWS Lambda started officially supporting Golang as a technology for creating functions. We’ll be able to leverage the Lambda SDK for Golang to do most of our work. However, we’ll also need to create our own data structures for working with Alexa requests and responses.

Let’s start by obtaining the AWS Lambda SDK for Golang. From the command line, execute the following:

go get github.com/aws/aws-lambda-go/lambda

The above command assumes that you have Go installed and your $GOPATH configured already. With the Lambda SDK available, we can start creating our function.

Within your $GOPATH path, create a main.go file with the following code:

package main

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/url"
    "strconv"
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
)

func Handler() (string, error) {
    return fmt.Sprintf("Hello World"), nil
}

func main() {
    lambda.Start(Handler)
}

The above code is the most basic of functions. When this is uploaded to Lambda and ran, the main function will start our only Handler function which will just return a string. While it isn’t exactly useful, we now know it doesn’t take much to use Lambda.

Before we even think about including Amazon Alexa into the mix, we should probably get our geocoding functional.

Convert Addresses to Latitude and Longitude Positions with the HERE Geocoder API

This isn’t the first time that I’ve written about using the HERE Geocoder API with Golang. If you’re interested in what you can do with HERE and Golang, you might want to check out my tutorial titled, Process CSV Address Information with Golang and the HERE Geocoder API. We’re going to reuse a lot of the logic that’s been seen in one of my many other Golang related tutorials.

To start, we’re going to focus on creating native data structures to hold information about the HERE platform as well as the response data. In the main.go file, include the following:

type HerePlatform struct {
    AppId   string
    AppCode string
}

type GeocoderResponse struct {
    Response struct {
        View []struct {
            Type   string `json:"_type"`
            Result []struct {
                Location struct {
                    Address struct {
                        Label       string `json:"Label,omitempty"`
                        Country     string `json:"Country,omitempty"`
                        State       string `json:"State,omitempty"`
                        County      string `json:"County,omitempty"`
                        City        string `json:"City,omitempty"`
                        District    string `json:"District,omitempty"`
                        Street      string `json:"Street,omitempty"`
                        HouseNumber string `json:"HouseNumber,omitempty"`
                        PostalCode  string `json:"PostalCode,omitempty"`
                    } `json:"Address"`
                    DisplayPosition struct {
                        Latitude  float64 `json:"Latitude"`
                        Longitude float64 `json:"Longitude"`
                    } `json:"DisplayPosition"`
                } `json:"Location"`
            } `json:"Result"`
        } `json:"View"`
    } `json:"Response"`
}

We have two different data structures in the above code. The HerePlatform data structure is designed to hold our application id and application code that can be obtained with a free HERE account. The GeocoderResponse is modeled after the response for the REST API. It doesn’t include everything, but it includes the things we care about for this example.

In the main function, we can update our code to look like the following:

var platform HerePlatform

func main() {
    platform = HerePlatform{AppId: "APP-ID-HERE", AppCode: "APP-CODE-HERE"}
    lambda.Start(Handler)
}

Notice that we are now configuring the HERE platform with our API tokens. Rather than creating some messy code, we should probably create a function that is responsible for making requests agains the API.

Within the main.go file, include the following function:

func (platform *HerePlatform) Geocode(query string) (GeocoderResponse, error) {
    endpoint, _ := url.Parse("https://geocoder.api.here.com/6.2/geocode.json")
    queryParams := endpoint.Query()
    queryParams.Set("app_id", platform.AppId)
    queryParams.Set("app_code", platform.AppCode)
    queryParams.Set("searchtext", query)
    endpoint.RawQuery = queryParams.Encode()
    response, err := http.Get(endpoint.String())
    if err != nil {
        return GeocoderResponse{}, err
    } else {
        data, _ := ioutil.ReadAll(response.Body)
        var geocoderResponse GeocoderResponse
        json.Unmarshal(data, &geocoderResponse)
        return geocoderResponse, nil
    }
}

The above Geocode function models the expectations of the HERE Geocoder API. We provide it the application id and application code that was previously defined and construct the request with a user defined query, which should be an address.

If the request to this function is successful, a properly modeled GeocoderResponse will be returned. We can make use of this function by altering the Handler function:

func Handler() (string, error) {
    geocoderResponse, _ := platform.Geocode("Tracy, CA")
    return fmt.Sprintf("%f, %f", geocoderResponse.Response.View[0].Result[0].Location.DisplayPosition.Latitude, geocoderResponse.Response.View[0].Result[0].Location.DisplayPosition.Longitude), nil
}

We didn’t do any error validation, but we’re assuming data was found for the query and as a response, the latitude and longitude is returned to the caller of this Lambda function.

To build this application for Lambda, execute the following:

GOOS=linux go build
zip handler.zip ./alexa-geocoder

There is an assumption with the above two commands. The assumption is that your project is called alexa-geocoder and that you’re using an operating system that has the zip command. Feel free to change it a bit or ZIP the binary however works best for you. The above commands worked fine for me on a Mac.

If we wanted to, we could upload this ZIP archive to AWS Lambda.

lambda-geocoder

Go through the steps of creating a Lambda function in your AWS Console and when prompted, say that you’re using Golang as the development technology. Eventually you’ll end up at a screen that looks like the one above. Add Alexa Skills Kit as a trigger and upload the ZIP file as your function code.

You can test the function by creating a test case in the top menu. It should return some latitude and longitude coordinates when you’re done.

Building, Uploading, and Testing the Application as an Alexa Skill

Now that we have a working Lambda function that makes use of the HERE Geocoder API, we can design it to work with Amazon Alexa. While Alexa can use Lambda, there are some configurations that must take place to make it successful.

Previously we returned a string as the response to our function. Alexa expects something a little different. I recommend installing the following package:

go get github.com/arienmalec/alexa-go

While not absolutely necessary, the above package acts as a solid blueprint for Alexa requests and responses. You could create your own as well, it is up to you.

Each Alexa Skill will have many different operations. This means our function needs to be able to process different requests. This is best solved by creating a type of dispatcher function that routes to other functions based on the incoming request.

Take the following:

func IntentDispatcher(request alexa.Request) alexa.Response {
    var response alexa.Response
    switch request.Body.Intent.Name {
    case "AddressIntent":
        response = HandleAddressIntent(request)
    case "AboutIntent":
        response = HandleAboutIntent(request)
    default:
        response = HandleAboutIntent(request)
    }
    return response
}

The above dispatcher will route to two potential functions containing logic. We’ll have an AboutIntent and an AddressIntent, both of which will do different things. The intent is determined by Amazon when receiving a request from the user. To call the IntentDispatcher function, our Handler function needs to change a bit:

func Handler(request alexa.Request) (alexa.Response, error) {
    return IntentDispatcher(request), nil
}

Notice that in both circumstances, we are now accepting an alexa.Request and returning an alexa.Response. These are both defined as part of the package that we downloaded.

Let’s start by creating the simplest of the two intents. Create a HandleAboutIntent as defined in the IntentDispatcher function:

func HandleAboutIntent(request alexa.Request) alexa.Response {
    return alexa.NewSimpleResponse("About", "Geocoder was created by Nic Raboy in Tracy, California")
}

When invoked, we just respond with a simple message. The NewSimpleResponse accepts two parameters. While Alexa only reads the second, two exist for populating card content within the Alexa mobile application.

Before we start working on the AddressIntent, we know what the HERE Geocoder API returns. We know that the latitude and longitude data is in float format, which isn’t the easiest to work with. Instead, we should create a function to convert that data.

func FloatToString(value float64) string {
    return strconv.FormatFloat(value, 'f', -1, 64)
}

The above function will convert our floats into strings. With that out of the way, let’s create an AddressIntent for working with user provided addresses and convert them into coordinates.

func HandleAddressIntent(request alexa.Request) alexa.Response {
    address := request.Body.Intent.Slots["Address"].Value
    if address == "" {
        return alexa.NewSimpleResponse("Geocoded Address", "An address is required, but was not found")
    }
    geocoderResponse, err := platform.Geocode(address)
    if err != nil {
        return alexa.NewSimpleResponse("Geocoded Address", err.Error())
    }
    if len(geocoderResponse.Response.View) == 0 || len(geocoderResponse.Response.View[0].Result) == 0 {
        return alexa.NewSimpleResponse("Geocoded Address", "No results found for "+address)
    }
    return alexa.NewSimpleResponse("Geocoded Address", FloatToString(geocoderResponse.Response.View[0].Result[0].Location.DisplayPosition.Latitude)+", "+FloatToString(geocoderResponse.Response.View[0].Result[0].Location.DisplayPosition.Longitude))
}

The setup of the HandleAddressIntent is the same, but now we’re working with slot data. The only slot for this intent is the actual address the user gives us. If it doesn’t exist, we will have Alexa say so. If it does exist, we will make a request to the HERE Geocoder API. If no result exists, we will return an error for Alexa to read. Otherwise, if everything looks good, we’ll return the latitude and longitude coordinate data.

Go ahead and build the application again. At this point we can upload the application in Lambda, and start connecting Alexa to the Lambda function.

After uploading the function, go to the Alexa Developer Console and create a new Skill.

alexa-skill-builder

As seen above, there will be a skill builder checklist of things that must be done before you can test and publish. Start by giving your Skill an invocation name, noting that it should be something Alexa will be able to understand when spoken.

Next you’ll want to create two custom intents to match what we have in our Go application. The intents should be named to match, so create an AboutIntent and an AddressIntent. Each intent will need a list of sample utterances. It is very important to list any and every possible utterance you can think of for calling these intents. The more sample sentences you provide, the more reliable your Skill will be.

For my AboutIntent, I’ve included the following:

who made this skill
who made this application
give me information
for some information

I’ve only listed a few, but you should have a lot more than that. The idea is that the Skill will be invoked with something like:

Alexa, ask [name] for some information

The sample utterance data will be slightly different for the AddressIntent because we have some slot information to provide. For the slot, we should name it Address and it should be of type AMAZON.StreetAddress. The utterances should look like the following:

get me the position of {Address}
give me the position of {Address}
find the coordinates of {Address}
find the position of {Address}

Notice that the above utterances have a variable defined by curly brackets. This variable must match the previously defined slot. The idea is that the user will be able to invoke the Skill with something like:

Alexa, ask [name] to get me the position of Tracy, California

The slot data can be much more complex than my example. Remember, Alexa is expecting any kind of address information. After defining your intents, you need to link up the Skill with Lambda. In Lambda, you’ll have access to the id for your function. Copy it and provide it to the Alexa Skill. When you’re done, you can build the Skill for testing.

geocoder-alexa-skill-test

Assuming everything went as plan, go ahead and either type or say some commands in the test area. The simulator should respond with actual answers as seen in the image above.

If you’re satisfied with everything that happened, you can publish your Skill.

Conclusion

You just saw how to create an Amazon Alexa Skill that takes address information and geocodes it to latitude and longitude coordinate information using Golang, AWS Lamda, and the HERE Geocoder API.

As someone who has developed and published Skills, the most difficult part is the deployment process, not the development process. This is similar to releasing a mobile application. There are a lot of steps involved. In terms of development, we just created some Golang functions that included HTTP request logic.