Tuesday, April 3, 2018

Google Dialogflow - Input Validation


Summary

This post concerns the task of validating user-entered input for a Google Dialogflow-driven chat agent.  My particular scenario is a quite simple/crude transactional flow, but I found input validation (slots) to be particularly cumbersome in Dialogflow.  Based on what I've seen in various forums, I'm not alone in that opinion.  Below are my thoughts on one way to handle input validation in Dialogflow.


Architecture

Below is high-level depiction of the Dialogflow architecture I utilized for my simple agent.  This particular agent is a repeat of something I did with AWS Lex (explanation here).  It's a firewood ordering agent.  The bot prompts for various items (number of cords, delivery address, etc) necessary to fulfill an order for firewood.  Really simple.



Below is my interpretation of the agent bot model in Dialogflow.

Validation Steps

For this simple, transactional agent I had various input items (slots) that needed to be provided by the end-user.  To validate those slots, I used two intents per item.  One intent was the main one that gathers the user's input.  That intent uses an input context to restrict access per the transactional flow.  The input to the intent is then be sent to a Google Cloud Function (GCF) for validation.  If it's valid, then a prompt is sent back to user for the next input slot.  If it's invalid, the GCF function triggers a follow-up intent to requery for that particular input item.  The user is trapped in that loop until they provide valid input.

Below is a diagram of the overall validation flow.


Below are screenshots of the Intent and requery-Intent for the 'number of cords' input item.  That item must be an integer between 1 and 3 for this simple scenario.



Code

Below is a depiction of the overall app architecture I used here.  All of the input validation is happening in a node.js function on GCF.


Validation function (firewoodWebhook.js)

The meaty parts of that function below:
function validate(data) { 
 console.log('validate: data.intentName - ' + data.metadata.intentName);
 switch (data.metadata.intentName) {
  case '3.0_getNumberCords':
   const cords = data.parameters.numberCords;
   if (cords && cords > 0 && cords < 4) {
    return new Promise((resolve, reject) => {
     const msg = 'We deliver within the 80863 zip code.  What is your street address?';
     const output = JSON.stringify({"speech": msg, "displayText": msg});
     resolve(output);
    });
   }
   else {
    return new Promise((resolve, reject) => {
     const output = JSON.stringify ({"followupEvent" : {"name":"requerynumbercords", "data":{}}});
     resolve(output);
    });
   }
   break;
  case '4.0_getStreet':
   const street = data.parameters.deliveryStreet;
   if (street) {
    return callStreetApi(street);
   }
   else {
    return new Promise((resolve, reject) => {
     const output = JSON.stringify ({"followupEvent" : {"name":"requerystreet", "data":{}}});
     resolve(output);
    });
   }
   break;
  case '5.0_getDeliveryTime':
   const dt = new Date(Date.parse(data.parameters.deliveryTime));
   const now = new Date();
   const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);
   const monthFromNow = new Date(now.getFullYear(), now.getMonth()+1, now.getDate());
   if (dt && dt.getUTCHours() >= 9 && dt.getUTCHours() <= 17 && dt >= tomorrow && dt <= monthFromNow) {
    return new Promise((resolve, reject) => {
     const contexts = data.contexts;
     let context = {};
     for (let i=0; i < contexts.length; i++){
      if (contexts[i].name === 'ordercontext') {
       context = contexts[i];
       break;
      }
     }
     const price = '$' + PRICE_PER_CORD[context.parameters.firewoodType] * context.parameters.numberCords;
     const msg = 'Thanks, your order for ' + context.parameters.numberCords + ' cords of ' + context.parameters.firewoodType + ' firewood ' + 
        'has been placed and will be delivered to ' + context.parameters.deliveryStreet + ' at ' + context.parameters.deliveryTime + '.  ' + 
        'We will need to collect a payment of ' + price + ' upon arrival.';
     const output = JSON.stringify({"speech": msg, "displayText": msg});
     resolve(output);
    });
   }
   else {
    return new Promise((resolve, reject) => {
     const output = JSON.stringify ({"followupEvent" : {"name":"requerydeliverytime", "data":{}}});
     resolve(output);   
    });
   }
   break;
  default:  //should never get here
   return new Promise((resolve, reject) => {
    const output = JSON.stringify ({"followupEvent" : {"name":"requestagent", "data":{}}});
    resolve(output);  
   });
 }
}
Focusing only on the number of cords validation -
Lines 6-11:  Check if the user input is between 1 and 3 cords.  If so, return a Promise object with the next prompt for input.
Lines 13-17:  Input is invalid.  Return a Promise object with a followupEvent to trigger the requery intent for this input item.

Client-side.  Dialogflow wrapper (dflow.js)

Meaty section of that below.  This is the 'send' function that submits user-input to Dialogflow for analysis and response.
 send(text) {
  const body = {'contexts': this.contexts,
      'query': text,
      'lang': 'en',
      'sessionId': this.sessionId
  };
  
  return fetch(this.url, {
   method: 'POST',
   body: JSON.stringify(body),
   headers: {'Content-Type' : 'application/json','Authorization' : 'Bearer ' + this.token},
   cache: 'no-store',
   mode: 'cors'
  })
  .then(response => response.json())
  .then(data => {
   console.log(data);
   if (data.status && data.status.code == 200) {
    this.contexts = data.result.contexts;
    return data.result.fulfillment.speech;
   }
   else {
    throw data.status.errorDetails;
   }
  })
  .catch(err => { 
   console.error(err);
   return 'We are experiencing technical difficulties.  Please contact an agent.';
  }) 
 }

Lines 8-29:  Main code here consists of a REST API call to Dialogflow with the user input.  If it's valid, return a Promise object with the next prompt.  Otherwise, send back a Promise with the error message.

Client-side.  User interface.

    function Chat(mode) {
        var _mode = mode;
     var _self = this;
        var _firstName;
        var _lastName;
        var _dflow; 
   
        this.start = function(firstName, lastName) {
            _firstName = firstName;
            _lastName = lastName;
            if (!_firstName || !_lastName) {
                alert('Please enter a first and last name');
                return;
            }
            
            _dflow = new DFlow("yourid");
            hide(getId('start'));
            show(getId('started'));
            getId('sendButton').disabled = false;
            getId('phrase').focus();
        };

        this.leave = function() {
         switch (_mode) {
          case 'dflow':       
           break;
         }
         getId('chat').innerHTML = '';
         show(getId('start'));
            hide(getId('started'));
            getId('firstName').focus();
        };
                       
        this.send = function() {
            var phrase = getId('phrase');
            var text = phrase.value.trim();
            phrase.value = '';

            if (text && text.length > 0) {
             var fromUser = _firstName + _lastName + ':'; 
             displayText(fromUser, text);
            
             switch (_mode) {
              case 'dflow':
               _dflow.send(text).then(resp => displayText('Bot:', resp));
               break;
             }
            }
        };         
Line 16:  Instantiate the Dialogflow wrapper object with your API token.
Line 45:  Call the 'send' function of the wrapper object and then display the returned text of the Promise.

Source Code


Copyright ©1993-2024 Joey E Whelan, All rights reserved.