III. Add fullfilment

Creating a fulfillment for an might seem to be a bit complicated at a glance, especially if you haven’t done any backend coding before. Don’t worry, me either! But it is much simpler than it sounds! So let’s begin, my daring friend! 😉

Enable webhook for an intent

Go to our assistant page in the Dialogflow then find the Fulfilment in a menu. Once you get there, you’ll see there are two options available: Webhook and Inline Editor. We will use the Inline Editor for simplicity. It utilizes Cloud Functions platform to run our backend code, but if you like to use your own hosting, choose Webhook. Click on a toggle next to the Inline Editor to enable it.

Now you should see a bit of a boilerplate code inside the editor. Delete it all for now. We will add it bit by bit, so you’ll get a better idea of what it’s for.

Add a standard dependency declaration at the top:

'use strict' // enables the ES5 features
const functions = require('firebase-functions')

Initialize the database

Those are the libraries to handle the low-level stuff like server creation and handling HTTP requests. Let’s also add Firebase DB support.

const admin = require('firebase-admin')
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: 'https://showoffices-7eed1.firebaseio.com/'
})

The first line is a Firebase DB dependency declaration. Then we initialize our app with the initializeApp() method. It takes two parameters: credential and databaseURL. Check out the Docks.

The credential is used to check whether you’re allowed to read/write to the DB. We use the applicationDefault() method here to get the credentials of the logged user.

The databaseURL is a route to our DB. You can find it in the Data tab of our Firebase DB.

Let’s create a reference to our database as db. We can also refer to the particular node of the database with a db.ref() method. Pass the desired node as an argument.

const db = admin.database()
const ref = db.ref("id_01")

Update database from code

Now we’re ready to write some data to our database. We will use ref.update()method. Let’s create some helper functionsetValue().

// declare the function
const setValue = (obj) => ref.update(obj)
// call it with object as an argument
setValue({countryName: "Gemany"});

Let’s deploy the fulfillment

Hit Deploy button at the bottom and wait while the deployment process goes. After a moment, you’ll see an error. To make it work, we need to add one more line. It initializes the server.

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {})

Just add it and hit Deploy once again. After the deployment is finished, go straight to the Firebase DB and see how the countryName value changed to "Germany". Now we could write data to our DB. Sweeeeet 🍤

Let’s take a look at thedialogflowFirebaseFulfillment function. It will receive an HTTP POST request each time the intent is invoked by a user. Let’s check that out. Add the following few lines inside the function body {}.

console.log('Request body: ' + JSON.stringify(request.body))

It’ll print the request inside the Firebase console. Hit Deploy once again. After it’s done type or say something in a simulator at the right panel. Then go to the Firebase, select the Functions tab at the left panel and then Logs. You’ll see the log of the request body in a JSON format.

Take a look at "parameters":{"countryName":"Ukraine"} inside the queryResult object. Those are the parameters of the invoked intent we need to push to our DB. We can write the following.

setValue(request.body.queryResult.parameters)

Test the result

Deploy once again and test in a simulator by saying: “Show me all offices”. Then go to the DB and see how the countryName value changed to "all"

As you can see It works and now we can update our DB and therefore prompt our client to rerender by simply speaking or chatting with the assistant. Yes, It’s that simple! But we can make it even better! 😱

For example, we can program different responses based on the parameters we got. For that, we need to send a response back to an assistant as simple text message or even include some media and formatting.

Initialize helping library

To simplify the task, Google provides us with a helper library called actions-on-google. Let’s add it to the declaration section at the top of our code. So now it should look like this:

'use strict'

const functions = require('firebase-functions')
const admin = require('firebase-admin')
const {dialogflow} = require('actions-on-google')

const app = dialogflow({debug: true}) // initialize app
// other code below

And change these lines:

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
setValue(request.body.queryResult.parameters)
})

to:

exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app)

Define invoked intent

To do something when the intent is triggered by a user we use app.intent() method, where we pass the intent name and the conv parameter which will store the data from an assistant regarding the intent (aka request.body).

app.intent('showOffices', (conv) => {
  // do things ..
})

Or we could use an alternative approach, use app.fallback() instead to catch all the request data from the assistant and then decide what to do (I prefer this method).

app.fallback((conv) => {
  const intent = conv.intent // catch the intent name

switch (intent) {
case 'showOffices':

      // do things ..

break;
}
})

Build response to the users

To give a user text response, we should use the conv.ask() method. Let’s use that to provide a user with a response to how many offices SoftServe has in different countries.

switch (intent) {
  case 'showOffices':
    const country = conv.parameters.countryName
const offices = {
"USA": 7, "Europe": 15, "Ukraine": 7, "Germany": 2,
"Poland": 4, "England": 1, "Bulgaria": 1
}
    if (country === "all") {
setValue(ref, {countryName: 'all'})
conv.ask('Here are all the SoftServe offices')

} else if (offices.hasOwnProperty(country)) {
setValue(ref, {countryName: country})
conv.ask(`There are ${offices[country]} offices in ${country}. To get back say: show me all offices`)

} else {
conv.ask('There is no SoftServe offices in that country')
}

  break;
}

As you can see we store the countryName as a variable each time the intent is invoked. Then we check if it passes different conditions to provide a user with a valid number of offices in each county he/she named.

Add multithreading

Since all the Framer clients are connected to the same DB node, every data change will be rendered on each and every client, what is not good.

To fix this problem we need to store the intent data under the assistant session and then somehow pass that session id to the Framer. Let’s update our code a bit:

app.fallback((conv) => {
  const sessionId = conv.contexts._session.split('/').splice(-1)[0]
const ref = db.ref(sessionId)
  // the rest of the code ..
})

First, we extracted the sessionId from the conv data. It’s generated by an assistant each time you start the conversation. Then we set a sessionId as a database ref node so that each data update will be pushed to that node, specific to the user session. Let’s also update the setValue() function.

const setValue = (ref, obj) => ref.update(obj)

Now to set countryName to "all" we should write the following:

setValue(ref, {countryName: 'all'})

After updating the code let’s Deploy once again and test it. 
For example, say: “Show me offices in England”. Then switch to DB and see how the new node was created.

Each time an assistant session is started, a new node will be created in a database. But we need to somehow pass the sessionId to the Framer because it’s referring to the id_01 DB node by default. We can use a query string in the URL to pass the id. Let’s use the welcome intent to generate the link for the user. My welcome intent is called openMap.

const framerURL = 'https://framer.cloud/BuOPt/' // initial URL
switch (intent) {
  case 'openMap':
setValue(ref, {countryName: 'all'}) // set initial value
conv.ask(framerURL+'?id='+sessionId) // generate the link
break;

}

Say “Hi” or “Hello” to invoked this intent. It’ll return you the link via the chat.

In my working prototype, we have a small app on the local machine, which opens a browser with a generated URL on the TV display inside the meeting room where conversation happens. But copying a link manualy is a nice workaround, don’t you thinks so?

Pass query parameters to the Framer project

As you can see there is a id=5887d53b-9a48–770e-a756–83abfec10119 parameter in the URL. That’s our sessionId that we need to extract from an address bar once the client is loaded. The easiest way of doing this is to use an awesome QueryInterface module by the Marc Krenn.

Let’s install the module via the Framer Module we already have.

To initialize the module add the following lines to the Framer project:

sessionId = new QueryInterface
key: "id"
default: "id_01"

Update the Firebase onChange event by putting the sessionId.value as a reference to the related node in DB. So now it will observe changes only in the particular node related to a user’s session.

demoDB.onChange “/#{sessionId.value}”, (value) ->

// other code below..

After the user opens the URL generated by our fulfillment the ?id= parameter will be extracted from the address bar and passed as a node-ref to the Firebase onChange event.



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here