Basic Fulfillment and Conversation Setup

In the previous guide, you built a basic weather agent that can recognize requests from users. In order to serve the actual information the user is requesting, you'll need to setup fulfillment, which requires deploying a service and calling an API.

Additionally, you want the agent to manage and repair the conversation if it doesn't go as expected, so you'll add some contexts and fallback intents.

Fulfillment (Webhook)

In order to add response logic or include the results of an API call in your agent's response, you need to setup fulfillment for the agent. This includes some basic JavaScript and setting up and hosting the files in a cloud service. This guide uses a Google Cloud Project for the hosting and deployment.

Create a Starter JS File

To start, create a directory on your local system for the code:

  • Linux or Mac OS X:

    mkdir ~/[PROJECT_NAME]
    cd ~/[PROJECT_NAME]
    
  • Windows:

    mkdir %HOMEPATH%[PROJECT_NAME]
    cd %HOMEPATH%[PROJECT_NAME]
    

Then create an index.js file in the project directory you just created, with the following code:

/*
* HTTP Cloud Function.
*
* @param {Object} req Cloud Function request context.
* @param {Object} res Cloud Function response context.
*/
exports.helloHttp = function helloHttp (req, res) {
  response = "This is a sample response from your webhook!" //Default response from the webhook to show it's working

  res.setHeader('Content-Type', 'application/json'); //Requires application/json MIME type
  res.send(JSON.stringify({ "speech": response, "displayText": response 
  //"speech" is the spoken version of the response, "displayText" is the visual version
  }));
};

Setup Google Cloud Project

  1. Follow "Before you begin" steps 1-5

  2. Deploy the function

    gcloud beta functions deploy helloHttp --stage-bucket [BUCKET_NAME] --trigger-http
    • helloHttp is the name of our project. You should have created your project in step 1 and set the project at the end of step 4 when you initialized gcloud.
    • --stage-bucket [BUCKET_NAME] can be found by going to your related Google Cloud project and click on Cloud Storage under the Resources section.
    • --trigger-http More information

Once completed, the status and information related to the function will be displayed. Make note of the httpsTrigger url. It should look something like this:

https://[REGION]-[PROJECT_ID].cloudfunctions.net/helloHttp

Enable Webhook in API.AI

  1. In API.AI, make sure you're in the correct agent and click on Fulfillment in the left hand menu
  2. Toggle the switch to enable the webhook for the agent
  3. In the URL text field, enter the httpTrigger url you got when you deployed your function
  4. Click Save

Enable Fulfillment in Intent

  1. Navigate to the "weather" intent
  2. Expand the Fulfillment section at the bottom of the page
  3. Check the Use Webhook option
  4. Click Save

Try it out

In the API.AI test console, enter "weather". You will see the webhook response we defined in the function. This means the webhook is working! You should also see the two parameters we need from our user, date and geo-city.

Setup Weather API

Get API Key

For this sample, we use the WWO (World Weather Online) service, so you'll need to get an API key. Once you register, login and make note of your API key.

Update Code

Now that we have an API key, we can make requests of the weather service and get actual data back from them.

Replace the current code in index.js with the code below. This adds communication with the weather service, our API key, and functions to handle our queries.

// Copyright 2017, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';
const http = require('http');
const host = 'api.worldweatheronline.com';
const wwoApiKey = '[YOUR_API_KEY]';
exports.weatherWebhook = (req, res) => {
  // Get the city and date from the request
  let city = req.body.result.parameters['geo-city']; // city is a required param
  // Get the date for the weather forecast (if present)
  let date = '';
  if (req.body.result.parameters['date']) {
    date = req.body.result.parameters['date'];
    console.log('Date: ' + date);
  }
  // Call the weather API
  callWeatherApi(city, date).then((output) => {
    // Return the results of the weather API to API.AI
    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({ 'speech': output, 'displayText': output }));
  }).catch((error) => {
    // If there is an error let the user know
    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({ 'speech': error, 'displayText': error }));
  });
};
function callWeatherApi (city, date) {
  return new Promise((resolve, reject) => {
    // Create the path for the HTTP request to get the weather
    let path = '/premium/v1/weather.ashx?format=json&num_of_days=1' +
      '&q=' + encodeURIComponent(city) + '&key=' + wwoApiKey + '&date=' + date;
    console.log('API Request: ' + host + path);
    // Make the HTTP request to get the weather
    http.get({host: host, path: path}, (res) => {
      let body = ''; // var to store the response chunks
      res.on('data', (d) => { body += d; }); // store each response chunk
      res.on('end', () => {
        // After all the data has been received parse the JSON for desired data
        let response = JSON.parse(body);
        let forecast = response['data']['weather'][0];
        let location = response['data']['request'][0];
        let conditions = response['data']['current_condition'][0];
        let currentConditions = conditions['weatherDesc'][0]['value'];
        // Create response
        let output = `Current conditions in the ${location['type']} 
        ${location['query']} are ${currentConditions} with a projected high of
        ${forecast['maxtempC']}°C or ${forecast['maxtempF']}°F and a low of 
        ${forecast['mintempC']}°C or ${forecast['mintempF']}°F on 
        ${forecast['date']}.`;
        // Resolve the promise with the output text
        console.log(output);
        resolve(output);
      });
      res.on('error', (error) => {
        reject(error);
      });
    });
  });
}

Deploy Function (again)

Now that our function is different, we need to deploy again and alter the command. This is due to the code having a new function name exported.

gcloud beta functions deploy weatherWebhook --stage-bucket [BUCKET_NAME] --trigger-http

Once the new function is deployed, make a note of the new httpTrigger url. It should look something like this:

https://[REGION]-[PROJECT_ID].cloudfunctions.net/weatherWebhook

Update Fulfillment in API.AI

  1. Return to API.AI and click on Fulfillment in the left hand menu
  2. Replace the current URL with the new httpTrigger url.

    https://[REGION]-[PROJECT_ID].cloudfunctions.net/weatherWebhook
  3. Click Save

Conversation Branching

We can't expect users will always provide all the information our agent needs to fulfill their request. In the case of our weather agent, a city and a date are required as inputs for our function. If no date is provided, we can assume the user is referring to "today" or the current date, but there's no way to gather the user's location or city through API.AI so we need to make sure we collect that.

Making Location Required

  1. In the "weather" intent, locate the geo-city parameter and check the Required option. This will reveal an additional column called Prompts. These are responses the agent will give when this specific data isn't provided by the user.
  2. Click on Define prompts and enter the following response:

    • For what city would you like the weather?
  3. Click Close
  4. Click Save on the Intent page

Give it a go!

Again, enter "weather" into the console and you should see the prompt to collect the city.

Add Location Context

In order to refer to data collected in a previous intent, we need to set an output context. In this case we want to "reuse" the value collected for location.

  1. In the "weather" intent, click on Contexts to expand the section
  2. In the Add output context field, type "location" and press "Enter" to commit the context
  3. Click Save

Create a New Intent for Context

We want to be able handle additional questions using the same location, without asking the user for the data again. Now that we've set an output context, we can use it as the input context for the intent that handles the additional questions.

  1. Click on Intents in the left hand menu or click the plus icon add to create a new intent
  2. Name the intent "weather.context"
  3. Set the input and output context as "location"
  4. Add the following User Says example:

    • What about tomorrow
  5. Add a new parameter with the following information:

    • Parameter Name: geo-city
    • Entity: empty
    • Value: #location.geo-city
  6. Add the following reply in the Response section:

    • "Sorry I don't know the weather for $date-period in #location.geo-city"
  7. Click on Fulfillment in the menu to expand the section and check the Use webhook option
  8. Click Save

Take it for a test drive!

In the API.AI console, enter "weather" and get the reply asking for the city. Then enter a city of your choosing. You'll see a response like the one above, which includes the data retrieved from the service. You can then ask questions like "What about tomorrow" to retrieve the forecast for that date.

Conversation Repair

Now that the main conversational function of our agent is complete, we want to make sure our agent welcomes the user and knows how to respond to requests that are not related to the weather.

Editing the Default Fallback Intent

When our user responds with an unrelated query, we want our agent to reply gracefully and direct the user back into "familiar territory." For this we'll edit the existing Default Fallback Intent.

  1. Click on Intents, then Default Fallback Intent
  2. Click on the trash can icon delete in the upper right hand corner of the Text Response table
  3. Click on Add Message Content and choose Text Response
  4. Enter the following responses:

    • I didn't understand. Can you try again?
    • I don't understand what you're saying. You can say things like "What's the weather in Paris today?" to get the weather forecast.
  5. Click Save

One more time!

Enter an unrelated request into the console and you'll get one of the two fallback responses.

Editing the Welcome Intent

Finally, we want our agent to greet users and maybe provide some ideas as to what they can ask.

  1. Click on Intents, then Default Welcome Intent
  2. Click on the trash can icon delete in the upper right hand corner of the Text Response table
  3. Click on Add Message Content and choose Text Response
  4. Enter the following response:

    • Welcome to Weather Bot! You can say things like "What's the weather in Mountain View tomorrow?" to get the weather forecast.
  5. Click Save

What's next?

In the final step we will review the completed agent and cover what can be done beyond what we've setup so far. (COMING SOON)