General Best Practices

Follow these best practices to ensure great conversation design.

Expect variations

Handle this in the "User says" input in Dialogflow. 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 Dialogflow 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);
}

Personalize the conversation with user preferences

Your app can ask users for their preferences and remember them for later use, letting you personalize future conversations with that user.

This example app gives users a weather report for a zip code. The following example code asks the user whether they'd like the app to remember their zip code for later conversations.

appMap.set("weather_report", function(app) {
   let zip = app.getArgument("zipcode");
   app.data.zip = zip;
   app.askForConfirmation(app.buildRichResponse()
      .addSimpleResponse(getWeatherReport(zip))
      .addSimpleResponse("Should I remember " + zip + " for next time?"));
});

appMap.set("remember_zip", function(app) {
   let confirmed = app.getArgument("CONFIRMATION");
   if (confirmed) {
      app.userStorage.zip = app.data.zip;
      app.tell("Great! See you next time.");
   } else app.tell("Ok, no problem.");
})

After asking the user which zip code they're in during their first dialog, you can skip that prompt during their next invocation and use the same zip code. You should still provide an escape route (e.g. a suggestion chip allowing them to pick a different zip code) but by reducing a turn of conversation in the common case, you create a much more seamless experience.

appMap.set("weather_report", function(app) {
   let zip = app.getArgument("zipcode");
   if (zip) app.tell(getWeatherReport(zip))
   else if (app.userStorage.zip) 
       app.ask(app.buildRichResponse()
             .addSimpleResponse(getWeatherReport(app.userStorage.zip))
             .addSuggestions("Try another zipcode"));
   else app.ask("What's your zip code?")
})

appMap.set("provide_zip", function(app) {
   app.userStorage.zip = app.getArgument("zipcode");
   app.tell(getWeatherReport(app.userStorage.zip));
})

Welcome users differently when they return

Maintaining some state between conversations ensures a far more natural experience for return users. The first step in crafting this experience is to greet return users differently. For instance, you can taper the greeting or surface useful information based on past conversations. To do this, use the incoming AppRequest.User lastSeen property to determine whether the user has interacted with your app before. If the lastSeen property is included in the request payload, you can use a different greeting than normal. The code below uses the Node.js client library to fetch the value of lastSeen.

// This function is used to handle the welcome intent
// In Dialogflow, the Default Welcome Intent ('input.welcome' action)
// In Actions SDK, the 'actions.intent.MAIN' intent
function welcome (app) {
  if (app.getLastSeen()) {
    app.ask(`Hey you're back...`);
  } else {
    app.ask('Welcome to Number Genie!...');
  }
}

You can further enhance this greeting by tailoring the response to the actual value of lastSeen. For instance, users whose last interaction occurred many months before the current interaction might receive a different greeting than those who used the app the day before.