Join the Actions on Google Developer Challenge to win a trip to Google I/O 2018 and more than 20 other prizes.

Best Practices

Follow these best practices to ensure great conversation design.

Expect variations

Handle this in the "User says" input in API.AI. Also, use more than one intent that can map to the same action, where each intent can be triggered with different sets of "User says" phrases.

Provide helpful reprompts and fail gracefully

To do this, initialize a fallbackCount variable in your app.data object, and it set to 0. Prepare an array of 3 fallback prompts (escalating in clarity), and a final fallback prompt that ends the conversation.

Then, create a fallback intent (ideally one for each actionable intent in the agent). In the intent handler, pull the fallback count from the app.data object, increment it, and if it is less than 3, pull the prompt from the array of 3. If the count is greater than 4, close the conversation using the final prompt. In all intents that are not fallbacks, reset the fallback count to 0. Ideally, templatize the fallbacks for specific intents to be specific to those.

const GENERAL_FALLBACK: [
    "Sorry, what was that?",
    "I didn't quite get that. I can tell you all about IO, like date or location, or about the sessions. What do you want to know about?",
    "I'm really sorry, I can't understand. Would you like to know where IO is or maybe hear about some sessions?"
  ];
const LIST_FALLBACK: [
    "Sorry, what was that?",
    "I didn't catch that. Could you tell me which one you liked?",
    "I'm having trouble understanding you. Which one of those did you like?"
  ];
const FINAL_FALLBACK: "I'm sorry I'm having trouble here. Maybe we should try this again later.";

function handleFallback (app, promptFetch, callback) {
  app.data.fallbackCount = parseInt(app.data.fallbackCount, 10);
  app.data.fallbackCount++;
  if (app.data.fallbackCount > 3) {
    app.tell(promptFetch.getFinalFallbackPrompt());
  } else {
    callback();
  }
}

// Intent handlers

function generalFallback (app) {
  handleFallback(app, promptFetch, () => {
    app.ask(GENERAL_FALLBACK[app.data.fallbackCount],
      getGeneralNoInputPrompts());
  });
}

function listFallback (app) {
  handleFallback(app, promptFetch, () => {
    app.ask(LIST_FALLBACK[app.data.fallbackCount],
      getGeneralNoInputPrompts());
  });
}

function nonFallback (app) {
  app.data.fallbackCount = 0;
  app.ask(...);
}

Be prepared to help at any time

Create an intent that listens for help phrases like "what can I do?", "what can you tell me", or "help". In this intent, offer some (rotating) response that offers an overview of what the agent can do and directs users to a possible action. Ideally, also use follow-up help intents in API.AI to create different help scenarios for different actionable intents.

CONST HELP_PROMPTS: [
    "There's a lot you might want to know about IO, and I can tell you all about it, like where it is and what the sessions are. What do you want to know?",
    "IO can be a little overwhelming, so I'm here to help. Let me know if you need any help figuring out the event, like when it is, or what the sessions are. What do you want to know?"
  ];

// Intent handler

function help (app) {
    ask(app, promptFetch.getHelpPrompt(), // fetches random entry from HELP_PROMPTS
      promptFetch.getGeneralNoInputPrompts());
}

Let users replay information

Wrap all your app.ask(output) methods with a proxy function that adds the output to app.data.lastPrompt. Create a repeat intent that listens for prompts to repeat from the user like "what?", "say that again", or "can you repeat that?". Create an array of repeat prefixes that can be used to acknowledge that the user asked for something to be repeated. In the repeat intent handler, call ask() with a concatenated string of the repeat prefix and the value of app.data.lastPrompt. Keep in mind that you'll have to shift any ssml opening tags if used in the last prompt.

const REPEAT_PREFIX: [
    "Sorry, I said ",
    "Let me repeat that. "
  ];

function ask (app, inputPrompt, noInputPrompts) {
  app.data.lastPrompt = inputPrompt;
  app.data.lastNoInputPrompts = noInputPrompts;
  app.ask(inputPrompt, noInputPrompts);
}

// Intent handlers

function normalIntent (app) {
  ask(app, "Hey this is a question", SOME_NO_INPUT_PROMPTS);
}

function repeat (app) {
    let repeatPrefix = promptFetch.getRepeatPrefix(); // randomly chooses from REPEAT_PREFIX
    // Move SSML start tags over
    if (app.data.lastPrompt.startsWith(promptFetch.getSSMLPrefix())) {
      app.data.lastPrompt =
        app.data.lastPrompt.slice(promptFetch.getSSMLPrefix().length);
      repeatPrefix = promptFetch.getSSMLPrefix() + repeatPrefix;
    }
    app.ask(repeatPrefix + app.data.lastPrompt,
      app.data.lastNoInputPrompts);
}

Welcome users differently when they return.

Keep an entry in a database for a user's user_id (provided in the request to your fulfillment). Then simply check for that entry when welcoming the user, in order to determine which prompt to use.

The code sample uses Firebase realtime DB.

const firebaseAdmin = require('firebase-admin');

// Import local JSON file as Cloud Function dependency
const cert = require('path/to/service/key.json');

firebaseAdmin.initializeApp({
  credential: firebaseAdmin.credential.cert(cert),
  databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
});

function encodeAsFirebaseKey (string) {
  return string.replace(/\%/g, '%25')
    .replace(/\./g, '%2E')
    .replace(/\#/g, '%23')
    .replace(/\$/g, '%24')
    .replace(/\//g, '%2F')
    .replace(/\[/g, '%5B')
    .replace(/\]/g, '%5D');
};

/*
 * Returns true if given user has invoked this action before.
 *
 * @return {boolean} True if the user is a previous user. False if first time user.
 */
function isPreviousUser (userId) {
    return new Promise((resolve, reject) => {
      firebaseAdmin.database().ref('users/' + encodeAsFirebaseKey(userId))
        .once('value', (data) => {
          if (data && data.val()) {
            resolve(true);
          } else {
            firebaseAdmin.database().ref('users/' + encodeAsFirebaseKey(userId)).set(true);
            resolve(false);
          }
        }, (error) => {
          reject(error);
        });
    });
}

// Calling code (intent handler)

function welcome (app) {
    return isPreviousUser(app.getUser().userId).then((userHasVisited) => {
        if (userHasVisited) {
          app.ask('Welcome to Number Genie!...',
             promptFetch.getGeneralNoInputPrompts());
        } else {
          app.ask('Hey you're back...',
            promptFetch.getGeneralNoInputPrompts());
        }
      });
  }