firebasedialogflow-esvui

How to continue the flow of dialog from the previous question


I have a scenario where the first question is "How many people are in 2018" and returns 2. I then want to continue the conversation by asking "How many male and female are there". It should return 1 male and 1 female without asking the year inferring form previous question.

How can Dialogflow and my fulfillment code infer the year?

Data

I have tried to get the total counts, and can also get the male and female counts, but I'm not sure how to continue the conversation in a natural way.

function countresponders2018(agent){
    const year = agent.parameters.year;
    return admin.database().ref('data').once('value')
      .then((snapshot) => {
        let totalCount=0;
        snapshot.forEach( childSnapshot => {
          var value = childSnapshot.val();
          if (value.SurveyYear==year){
          totalCount+=1;  
          }
        });
            agent.add(`The total responders are ${totalCount}`);
        
    });


Solution

  • This is a great question, and important to think about when designing good conversations.

    I'm going to ignore the database queries, since it sounds like you may have this part under control, and it isn't directly related to the conversational parts.

    Restating the problem

    To rephrase what you want to do, you'd like to handle all the following phrases the same way:

    Similarly, these should all be the same:

    ie - users should be able to ask questions, both specifying the year, and possibly without.

    During the conversation, if they specify the year, this value should be the one assumed for further questions unless they specify a different year. This is pretty natural - humans build up short-term context in our conversations.

    These also have an issue you didn't raise in your question - how to handle a question if they haven't yet specified a year. Good conversation design would suggest that you should ask for the year, and then execute the question that was originally asked. So the conversation might be something like this:

    User:  How many people are there?
    Agent: What year would you like that for?
    User:  2018
    Agent: There were 2 people in 2018
    

    Overall approach: Contexts

    Fortunately - Dialogflow supports this concept directly with Contexts. This lets you save parameters in between statements from the user, so this can be a good way to store the year for later questions.

    You an also make Intents that can only be triggered when specific Contexts are active. This can be useful to make sure which Intents make sense at parts of the conversation.

    There are two good approaches to using Contexts for these kinds of questions. Although each has trade-offs, which one you use is largely a matter of personal style. You can also use contexts to support the scenario where you need to ask for the year if we don't already have it.

    Approach 1: Single Intent per question

    With this scheme, you would have a single Intent that responds to each question from the user. Your fulfillment would see if the year parameter is set and, if so, use that parameter as the year and set it in a Context's parameters. If it isn't set - then use the value from the parameters in the Context.

    So our "askPeople" Intent might have the phrases we talked about above:

    And we define "year" as a parameter of the @sys.number-integer for example. (Other entity types are possible, but this will do for now.)

    If you're using the dialogflow-fulfillment library, our handler function for this might look something like this:

    function askPeople( agent ){
      // Try to get the year from the context first
      let context = agent.context.get('RememberYear');
      let year = context && context.parameters && context.parameters.year;
    
      // If we don't have a year, get it from the phrase parameters
      year = year || agent.parameters.year;
    
      if( year ){
        // If we do have a value, get the result, reply,
        // and save the year in the context.
        return getPeople( year )
          .then( count => replyPeople( agent, year, count ) );
    
      } else {
        // We don't have a value
        // FIXME: We'll discuss what to do about this below
      }
    }
    

    You'll need to write the getPeople() function to return a Promise that resolves to the results from your database. I also pulled out the replyPeople() function intentionally. It might look something like this

    function replyPeople( agent, year, count ){
      agent.context.set({
        name: 'RememberYear',
        lifespan: 99,
        parameters:{
          year: year
        }
      });
      agent.add( `The total count for ${year} was ${count}.` );
    }
    

    Approach 2: Multiple Intents per question

    With this, we will have two different Intents that handle the question. One accepts it with the year, while the other handles the phrase without the year. The big difference is that the one without the year in the training phrase will require the "RememberYear" Context to be set in order to trigger.

    The base Intent ("askPeopleYear") is the fairly familiar one with training phrases such as

    And the "year" parameter defined the same way as above.

    Our other Intent ("askPeopleNoYear") would have the Input Context of "RememberYear" set and have a training phrase such as

    And would not have any parameters.

    We'll likely need a third Intent, or at least additional way to deal with, what happens if the "RememberYear" Context isn't set, but they say that phrase. We'll discuss this below.

    The fulfillment code would require two different handler functions that might look something like this

    function askPeopleYear( agent ){
      // We know the year is in the parameter
      let year = agent.parameters.year;
    
      // Get the result, reply, and set the context
      return getPeople( year )
        .then( count => replyPeople( agent, year, count ) );
    }
    
    function askPeopleNoYear( agent ){
      // We know the year is in the context
      let context = agent.context.get('RememberYear');
      let year = context && context.parameters && context.parameters.year;
    
      // Get the result, reply, and set the context
      return getPeople( year )
        .then( count => replyPeople( agent, year, count ) );
    }
    

    The getPeople() and replyPeople() functions would be the same as in our previous approach.

    Dealing with no year set

    So what happens if they say "How many people are there", but we don't have the "RememberYear" Context set with a value?

    In the first approach, this gets to the else condition which we marked with a "FIXME". In the second approach, a Fallback Intent would be triggered if we don't put something else in place, and this doesn't really help the user.

    In either case, what we should do is ask the user for the year they want and create an Intent to capture the year. Since we may need to do this for different question types, we should also store which function to execute in a... you guessed it... Context. So let's assume that we set a "NeedsYear" Context with a parameter named "functionName" to track which function we'll need to call.

    This Intent (let's name it "provideYear") would require the "NeedsYear" Input Context and might have training phrases such as:

    And take a "year" parameter, same as we've defined above. (We might even mark this as required.)

    The handler for this might look something like

    function provideYear( agent ){
      // We know we have the parmeter
      let year = agent.parameters.year;
    
      // We also know we have the context with the function name
      let context = agent.context.get('NeedsYear');
      let functionName = context && context.parameters && context.parameters.functionName;
    
      // We should clear this context, since we no longer need the year
      agent.context.set({
        name: 'NeedsYear',
        lifespan: 0
      };
    
      // And then call whichever function is appropriate
      if( functionName === 'people' ){
        return getPeople( year )
          .then( count => replyPeople( agent, year, count ) );
    
      } else if( functionName === 'gender' ){
        // ...
      }
    }
    

    Summary

    Use Contexts.

    Contexts serve as good gatekeepers for which Intents can be triggered, and also a good way to store values between turns of a conversation.

    Contexts are powerful.