Tutorial: A skill utilizing an external API

Introduction

In this tutorial, you will learn how to build a basic interactive skill connecting to an external API - Wolfram Alpha's spoken response API. If you want to jump ahead to a working full code-example, please see WolframAlpha on Github.

This tutorial assumes that you:

Goal of the skill

In this tutorial, we want to walk you through the process of calling an external API from your skill. You can really hook up any kind of API you like, but to showcase a common use-case, we will integrate Wolfram Alpha's Spoken Answers API. This is an API that provides answers to a large range of questions, mostly of mathematical/scientific art. The API is simple - for each question you ask it you get an answer. The answer might in some cases be that no result is available or that Wolfram Alpha didn't understand the input. These cases need to be handled in a special way.

Creating and importing the skill into your IDE

Follow the same principles as in the previous tutorial to create and import the skill into IntelliJ IDEA.

Note: Note that you can, if you prefer, instead download and import the complete example skill from Github. Instructions on importing are available in the repo's readme, or in the above link to the first tutorial.

Interaction triggers

As walked through in the previous tutorial, the skill template contains boilerplate handlers for interaction triggers based on users entering and leaving the robot's interaction space. This will be sufficient here aswell.

Thus, we can focus on the active part of the interaction, starting with the Start state.

Fetching question from user

To start off our skill, once the interaction has started we simply want to greet the user and ask if he/she has a question for us. Then, we want to catch the response we get, with special handlers for Yes and No intents.

In this tutorial we will let Wolfram Alpha handle any speech response we get, i.e. we will not use any natural language processing to preprocess the input to try to determine if it is a question or not. Thus, we catch a "naked" onResponse where we will do the API call.

We start of like this:

val Start : State = state(Interaction) {

    onEntry {
        furhat.ask("Hi there! Do you have any question?")
    }

    onResponse<Yes>{
        furhat.ask("What is it?")
    }

    onResponse<No>{
        furhat.say("Okay, no worries")
        goto(Idle)
    }

    onResponse {
        // Handle question here
    }
}

For now, nothing special is happening here. The last onResponse will catch any response that has not already been caught (i.e the Yes and No intents). If you want to test this, you can simply have Furhat repeat the input with furhat.say(it.text) where it refers to the response object or print the same using println(it.text).

Getting access to the Wolfram Alpha API

Now, we head over to Wolfram Alpha and check out their API explorer for Spoken results.

We note that API requests are of the following format https://api.wolframalpha.com/v1/spoken?i=How+old+is+Michael+Jordan%3F&appid=DEMO

We see that we need to pass the question and an application id in a query-string with an i parameter with the question delimited by "+" and a appid parameter with the application id. We create a free account and acquire our app id and are then ready to roll!

Setting up a HTTP library - khttp

Next up, we realize we need a http library to call the API. You can use whatever you prefer here, but for this tutorial we will use khttp - a simple and neat Kotlin http library (similar to Python's requests module according to the author).

We see that khttp is available on Jcenter (which already exist in the default skill template's repositories list) so we only need to add it as a dependency to add it to our skill. This is done with the following line on the bottom dependencies (i.e. not to the buildscript { ... }) part of our build.gradle file:

dependencies {
    // ...
    compile 'khttp:khttp:0.1.0'
}

Gradle will now fetch the library for you.

Querying the API

Querying the API with khttp is easy, we just have to make sure we build the right url and then use khttp.get(url).text to do the GET request and get the answer as a text String.

To build the URL we define a few constants:

val BASE_URL = "https://api.wolframalpha.com/v1/spoken"
val APP_ID = "YOUR_APP_ID_HERE"

We then build the url and the query. We have to do some string manipulation since the API requires words to be "+"-separated. Assuming that the API parses "+" and "plus" as the same, we replace any "+" signs with "plus" and then in turn replace all spaces with "+". We then patch together our query url:

val question = it.text.replace("+", " plus ").replace(" ", "+")
val query = "$BASE_URL?i=$question&appid=$APP_ID"

The API call, along with replying to the user with the given response from the API, is then done with:

// Call API
val response = khttp.get(query).text

// Reply to the user with the given response and allow them to ask a followup question
furhat.say(response)
furhat.ask("Anything else?")

Since API calls might take time, we want to do things to make the interaction better. First, we want to add a filler speech and gesture to signal to the user that an answer is coming shortly. So, before we do the API call, we do this:

// Filler speech and gesture
furhat.say({
    +"Let's see"
    +Gestures.GazeAway
}, async = true)

// API call
val response = khttp.get(query).text

// ...

This syntax is using an (inline definition of a) utterance, which combines a say and a gesture in one command and then in this case, executes them asynchronously (through the async = true parameter) since we want the API-call to be run immediately after.

Secondly, we want to handle potential timeouts of the API, or slow response times. This can be done in several ways but we recommend using a specific state for the API-call together with an anonymous sub-state for the actual API-call and a timer that cancels the call if the API-call hasn't returned within a certain time.

Our response handler that takes the user input now looks like this:

onResponse {
  // Filler speech and gesture
  furhat.say({
      +"Let's see"
      +Gestures.GazeAway
  }, async = true)

  // Query done in query state below, with its result saved here since we're doing a call
  val response = call(Query(it.text)) as String

  // Reply to user
  furhat.say(response)
  furhat.ask("Anything else?")
}

Instead of doing the API call, we are calling a state Query with the user input and then saving the returned value of this state call in a response variable. Note that the returned value needs to be cast to a String (with as String) since Kotlin's type inference doesn't work here.

Our Query state is now defined as follows:

// Constant for timeout of API call
val TIMEOUT = 4000

fun Query(question: String) = state {
  onEntry {
    // Query building
    val question = question.replace("+", " plus ").replace(" ", "+")
    val query = "$BASE_URL?i=$question&appid=$APP_ID"

    // Calling API (in an anonymous sub-state)
    val response = call {
      khttp.get(query).text
    } as String

    // Return the response
    terminate(response)
  }

  onTime(TIMEOUT) {
    // If timeout is reached, we return an error
    terminate("I'm having issues connecting to my brain. Try again later!")
  }
}

The call to the API is here made in an anonymous sub-state (https://docs.furhat.io/flow/#calling-anonymous-states) through the call { ... } to allow our timeout (onTime(TIMEOUT)) to stop the call if it takes to long. This is needed since otherwise the API-call would block any other event from happening which would cause the system to be unresponsive until a response is received. Note that here as-well, you explicitly have to cast the result of the state to a String.

Once we get a result, we return it to the caller state through the terminate call.

The onTime, that currently uses the TIMEOUT constant set to 4 seconds, makes sure we don't get stuck here if the API stops responding. Once it executes, it will terminate the state (including the called anonymous sub-state) providing a hard-coded error message.

Error handling

Finally, we want to catch some of the error messages that Wolfram Alpha returns if they can't parse the question or can't answer it.

We add two known error strings to a list and checks if the returned response matches, in which case we reply with a custom error message.

val FAILED_RESPONSES = listOf("No spoken result available", "Wolfram Alpha did not understand your input")

// ...

// Api Call
val response = call {
  khttp.get(query).text
} as String

// Error handling
val reply = when {
  FAILED_RESPONSES.contains(response) -> {
    println("No answer to question: $question")
    "Sorry bro, can't answer that"
  }
  else -> response
}

// Return the response
terminate(reply)

This concludes this tutorial. For a working code-example, please see Github.