How to Get Started With Google Actions

Continued from page 3. 

When the user puts in phrases, either there is a match for an Intent or not. If there is a match like we have seen for the Population Info Intent, then the Intent is invoked. But if there is no match, then it will go to the Fallback Intent.

Every phrase that the user provides will be available in the Training section and for those intents where there was no match, you can actually map it to an existing intent. This will help you map multiple ways that the user can ask for information.

Fulfillment Webhook and Deployment

The Agent that we wrote did not call any backend logic. The process so far was to demonstrate the process of writing an Agent using API.AI and providing a canned response. In fact, if you want to connect the action to your backend logic, the integration is straightforward and occurs via a Webhook. Let us go through this step by step:

Backend Code to invoke Population.io API

As mentioned earlier, the JSON Request that will be sent to your webhook and specifically the one with the parameters is shown below (partial response):

{
 "id": "6ff903f3-c44d-4e49-a8ba-1f31d17b2d39",
 "timestamp": "2017-01-15T11:30:27.595Z",
 "result": {
   "source": "agent",
   "resolvedQuery": "25",
   "action": "",
   "actionIncomplete": false,
   "parameters": {
     "age": "25",
     "geo-country": "Mexico",
     "year": "2001"
   },
   "contexts": [],
 ...
 ...
]

So we need to extract out the parameters and then use that to invoke the population.io API endpoint.

A sample backend code in Go language (populationapi.go) that invokes the population.io API endpoint is shown below.

package main

import (

    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

type APIAIRequest struct {
    ID        string    `json:"id"`
    Timestamp time.Time `json:"timestamp"`
    Result    struct {
        Parameters map[string]string `json:"parameters"`
        Contexts   []interface{}     `json:"contexts"`
        Metadata   struct {
            IntentID                  string `json:"intentId"`
            WebhookUsed               string `json:"webhookUsed"`
            WebhookForSlotFillingUsed string `json:"webhookForSlotFillingUsed"`
            IntentName                string `json:"intentName"`
        } `json:"metadata"`
        Score float32 `json:"score"`
    } `json:"result"`
    Status struct {
        Code      int    `json:"code"`
        ErrorType string `json:"errorType"`
    } `json:"status"`
    SessionID       string      `json:"sessionId"`
    OriginalRequest interface{} `json:"originalRequest"`
}

//API.AI Response
type APIAIMessage struct {
    Speech      string `json:"speech"`
    DisplayText string `json:"displayText"`
    Source      string `json:"source"`
}

//Population.io API Response
type YearCountryAgeResponse struct {
    Females int
    Males   int
    Country string
    Year    int
    Total   int
    Age     int
}

type PopulationAPIResp []YearCountryAgeResponse

func APIAIPopulationEndpoint(w http.ResponseWriter, req *http.Request) {

    if req.Method == "POST" {
        decoder := json.NewDecoder(req.Body)

        var t APIAIRequest
        err := decoder.Decode(&t)
        if err != nil {
            fmt.Println(err)
            http.Error(w, "Error in decoding the Request data", http.StatusInternalServerError)
        }

        log.Println(t.Result.Parameters["geo-country"], t.Result.Parameters["year"], t.Result.Parameters["age"])
        country := t.Result.Parameters["geo-country"]
        year := t.Result.Parameters["year"]
        age := t.Result.Parameters["age"]

        //Now make a call to the external Population.io Example
        apiResponse, err := http.Get("http://api.population.io/1.0/population/" + year + "/" + country + "/" + age + "/?format=json")
        if err != nil {
            fmt.Println(err)
            http.Error(w, "Error in decoding the Request data", http.StatusInternalServerError)
        }
        defer apiResponse.Body.Close()

        //Read the Response and unmarshall it into our Status struct
        body, _ := ioutil.ReadAll(apiResponse.Body)
        var populationAPIResponse PopulationAPIResp
        err = json.Unmarshal(body, &populationAPIResponse)
        if err != nil {
            fmt.Println(err)
            http.Error(w, "Error in decoding the Request data", http.StatusInternalServerError)
        }

        //Create Response Message
        speechResponse := fmt.Sprintf("In %s, the Population Statistics shows a total of %d males and %d females in %s in the age group of %s years", year, populationAPIResponse[0].Males, populationAPIResponse[0].Females, country, age)
        textResponse := fmt.Sprintf("Total males: %d, Total females: %d", populationAPIResponse[0].Males, populationAPIResponse[0].Females)
        msg := APIAIMessage{Source: "Population.io API", Speech: speechResponse, DisplayText: textResponse}

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(msg)
    } else {
        http.Error(w, "Invalid Request Method", http.StatusMethodNotAllowed)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/apiai", APIAIPopulationEndpoint)
    log.Fatal(http.ListenAndServe(":5000", mux))

}

Using ngrok to locally run the Webhook

ngrok is a utility that helps you expose a local server to the Internet. Please download and install the utility. Once it is available on your development server, where you have the API code, we do the following:

1. We start up the Go application, which exposes the API Server via go run populationapi.go

$ go run populationapi.go

2. We start ngrok to expose a secure public tunnel on port 9000 via the following command:

$ ngrok http 9000

This will start the ngrok utility and  you will see an output similar to the following. It shows the http/https endpoints that it has exposed and where it tunnels the traffic to i.e. localhost running on port 9000. Now, all you need to do is use this Forwarding endpoint in the next section.

Session Status                online
Version                       2.1.18
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://8165e270.ngrok.io -> localhost:9000
Forwarding                    https://8165e270.ngrok.io -> localhost:9000

Configuring the Fulfillment Webhook

All you will need to do is go to the Fulfillment option in the main menu and slide the Enabled option to “on”.

This will bring up the Webhook form, where you can fill out the details of your endpoint where the request will be processed and the appropriate response will be provided. You will need to read up on webhook requirements where the documentation mentions how you would be getting the request and the response format that is expected by the API.AI platform.

Romin Irani Romin loves learning about new technologies and teaching it to others. His passion is to help developers succeed.
 

Comments (3)

girishlal

I followed the tutorial and build the app. But in simulator I always get "Sorry, this action is not available in simulation”,any ideas you can suggest to resolve that? ( A.API and Simulator google account are same)

romin

I have faced this issue (occasionally) and have found it to be one of the following:

  1. A temporary problem with the service for testing that caused the problem.
  2. Multiple Google Accounts. I used to ensure that I was logged out then of all my accounts and logged in with the particular Google Account that I wanted to use.

Thanks,

Romin