Wednesday, May 30, 2018

Developing a Google Assistant app with NodeJs, Dialogflow & Firebase Functions - Part 3

This is part of a multipart series. 
This is part 3. 

Setting up the project.
  1. Install firebase-cli. In many cases, new features and bug fixes are available only with the latest version of the Firebase CLI and thefirebase-functions SDK. So first install the latest versions of firebase-functions and firebase-admin
npm install firebase-functions@latest firebase-admin@latest --save
npm install -g firebase-tools
2. Once the dependencies are installed, do the following to initialize the project
a. Run firebase login to log in via the browser and authenticate the firebase tool.
b. Go to your Firebase project directory.
c. Run firebase init. The tool gives you an option to install dependencies with npm. We will select functions for the function and hosting for any public files. The init tool will guide through various options to choose the language, and the initialization options for functions and hosting

myproject
 +- .firebaserc    # Hidden file that helps you quickly switch between
 |                 # projects with `firebase use`
 |
 +- firebase.json  # Describes properties for your project
 |
 +- functions/     # Directory containing all your functions code
      |
      +- .eslintrc.json  # Optional file containing rules for JavaScript linting.
      |
      +- package.json  # npm package file describing your Cloud Functions code
      |
      +- index.js      # main source file for your Cloud Functions code
      |
      +- node_modules/ # directory where your dependencies (declared in
                       # package.json) are installed


Once the project is initialized, we can develop our functions.
This is the package.json for the dependencies we have. We are using actions-on-google, firebase-admin, firebase-functions and google cloud datastore. We are using es-lint for linting. Code linting is a type of static analysis that is frequently used to find problematic patterns or code that doesn’t adhere to certain style guidelines.
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "lint": "eslint --fix \"**/*.js\"",
    "start": "firebase serve",
    "deploy": "firebase deploy",
    "test": "npm run lint"
  },
  "dependencies": {
    "@google-cloud/datastore": "1.1.0",
    "actions-on-google": "^2.0.0",
    "ajv": "^5.0.0",
    "firebase-admin": "^5.11.0",
    "firebase-functions": "^1.0.0"
  },
  "devDependencies": {
    "eslint": "^4.19.1",
    "eslint-config-google": "^0.9.1"
  },
  "private": true,
  "version": "0.0.1"
}
Next let us look at the functions. When user invokes the app by name or google matches the request to any of our intents, then it sends a request to our app with the intent and other parameters. In our function we will implement the callback function for each intent. 
'use strict';
//Initialize libraries
const {dialogflow} = require('actions-on-google');
const functions = require('firebase-functions');
const Datastore = require('@google-cloud/datastore');
const {
  SimpleResponse,
  BasicCard,
  Image,
  Suggestions,
  Button
} = require('actions-on-google');
// Instantiate a datastore client
const datastore = Datastore();
  
  const app = dialogflow({debug: true});
app.middleware((conv) => {
    
  });
//Setup contexts
const Contexts = {
    ONE_MORE: 'one_more'
  };
app.intent('quit_app', (conv) => {
    conv.close("Have a good day! come back again. Bye!");
  });
app.intent('start_app', (conv) => {
    conv.contexts.set(Contexts.ONE_MORE,5);
    const initMessage = ` Welcome to LitInspire. With great quotes and inspiring passages, I will inspire you.`;
return  getQuote().then((entity)=>{
         return getMessageFromQuote(entity,initMessage,conv);
    });
      
  });
app.intent('one_more_yes', (conv) => {
    conv.contexts.set(Contexts.ONE_MORE,3);
      const initMessage = `Great! Here is another one.`;
         
    return  getQuote().then((entity)=>{
      return getMessageFromQuote(entity,initMessage,conv);
  });
      
  });
app.intent('one_more_no', (conv) => {
    conv.close("Hope you're inspired and ready to take on your challenges. Have a good day and come back for more.");
});
app.intent('Default Fallback Intent', (conv) => {
    console.log(conv.data.fallbackCount);
    if (typeof conv.data.fallbackCount !== 'number') {
      conv.data.fallbackCount = 0;
    }
    conv.data.fallbackCount++;
    // Provide two prompts before ending game
    if (conv.data.fallbackCount === 1) {
      conv.contexts.set(Contexts.ONE_MORE,2);
      return conv.ask(new Suggestions('Yes Please', 'No thanks'), new SimpleResponse("Would you like to hear a quote?"));
    }else if(conv.data.fallbackCount === 2){
      return conv.ask(new Suggestions('Yes Please', 'No thanks'), new SimpleResponse("Welcome to LitInspire. With great quotes and inspiring passages, I will inspire you.Would you like to hear a quote?"));
    }
   return conv.close("This isn't working.Have a good day. Bye! ");
});
function getRandomNumber(){
return  Math.floor((Math.random()*num_quotes)+1);
}

function buildReadableQuoteFromEntity(entity){
  let readableQuote =  entity.quote + 
     ` This was said by ` + entity.author + ` `  ;
     if(entity.comments){
       readableQuote +=  entity.comments + ` `;
     }
     return readableQuote;
}
function getViewableQuote(entity){
  let viewableQuote =  entity.quote + 
     `.This was said by ` + entity.author + ` `  ;
     if(entity.comments){
      viewableQuote +=  entity.comments + ` `;
     }
     return viewableQuote;
}
function getEndingMessage(){
return `  
" clipBegin="10s" clipEnd="13s">Consider the quote!
Do you want to listen to another quote?`;
}
function getEndingMessageText(){
  return `.Do you want to listen to another quote?`;
  }
function getMessageFromQuote(entity,initMessage,conv){
  return conv.ask(new Suggestions('Yes Please', 'No thanks'), new SimpleResponse(initMessage),
  new SimpleResponse( {text: getViewableQuote(entity) + getEndingMessageText(),
speech: ` ` +  buildReadableQuoteFromEntity(entity)   + getEndingMessage() + `   ` }));
 }
function getQuote(){
  return new Promise(((resolve,reject) => {
    let randomQuoteNum = getRandomNumber();
  console.log("the id of the quote is: quote_"+randomQuoteNum);
  const key = datastore.key(['quote', 'quote_'+randomQuoteNum]);
  console.log("Querying datastore for the quote..."+key);
  let readableQuote = '';
  datastore.get(key,(err,entity) => {
    if(!err){
      console.log('entity:'+entity.quote);
    resolve(entity);
    }else{
     reject(console.log('Error occured'));
    }
  });
  }));
}
// HTTP Cloud Function for Firebase handler
exports.InspireMe = functions.https.onRequest(app);
Let us look at the intent handler for the start_app. First since we are in the app, we want the user to be able to ask for multiple quotes. Since it is in the one_more context, we will set the context to one_more. Next we need to get a quote and return it. We have the quotes stored in google cloud datastore. So we make a call to the datastore to return a random quote. 
app.intent('start_app', (conv) => {
    conv.contexts.set(Contexts.ONE_MORE,5);
    const initMessage = ` Welcome to LitInspire. With great quotes and inspiring passages, I will inspire you.`;
return  getQuote().then((entity)=>{
         return getMessageFromQuote(entity,initMessage,conv);
    });
      
  });
The getQuote() function returns a promise with the quote. And in the intent handler, we use the .then() function of the promise to build the message from quote and return it. In the getMessageFromQuote() method, we can see join together multiple responses and return it. Here we use the ask method, which tells the user our quote and waits for the user response. And in this method we can pass suggestions, and atmost two simple responses. The simple response converts the text to speech. We are using the SSML (speech synthesis markup lanuguage) to specify how to generate the speech. With SSML, we can specify where to pause, and add music to the text. There are other types of responses also like Basic Card, Image, Button and List (Carousel) responses. 
function getMessageFromQuote(entity,initMessage,conv){
  return conv.ask(new Suggestions('Yes Please', 'No thanks'), new SimpleResponse(initMessage),
  new SimpleResponse( {text: getViewableQuote(entity) + getEndingMessageText(),
speech: ` ` +  buildReadableQuoteFromEntity(entity)   + getEndingMessage() + `   ` }));
 }
The two other things to see are how to end the conversation and how to handle unknown input. In the below function you can see that when the user says no, we use the conv.close() function to end the conversation with a message. 
app.intent('one_more_no', (conv) => {
    conv.close("Hope you're inspired and ready to take on your challenges. Have a good day and come back for more.");
});
When the user provides unknown input, google invokes the default fallback function. Lets look at that. We are giving options to the user two times whether to listen to a quote or not. And if the user doesn’t give a valid response even after two times, we are ending the conversation. 
app.intent('Default Fallback Intent', (conv) => {
    console.log(conv.data.fallbackCount);
    if (typeof conv.data.fallbackCount !== 'number') {
      conv.data.fallbackCount = 0;
    }
    conv.data.fallbackCount++;
    // Provide two prompts before ending game
    if (conv.data.fallbackCount === 1) {
      conv.contexts.set(Contexts.ONE_MORE,2);
      return conv.ask(new Suggestions('Yes Please', 'No thanks'), new SimpleResponse("Would you like to hear a quote?"));
    }else if(conv.data.fallbackCount === 2){
      return conv.ask(new Suggestions('Yes Please', 'No thanks'), new SimpleResponse("Welcome to LitInspire. With great quotes and inspiring passages, I will inspire you.Would you like to hear a quote?"));
    }
   return conv.close("This isn't working.Have a good day. Bye! ");
});
Now that’s it. We have written intent handlers for all possible intents and fallback handler also. Now this is ready for testing. 
Use the firebase deploy command to deploy the function to firebase functions. 
firebase deploy
Once deployed you will get a functions url. Go to the dialogflow console and go to the fulfilment menu item and provide the function url as the fulfilment url. 

Enter fulfilment

No comments:

Post a Comment

Comments