Creating a slow reply from Dialogflow - dialogflow-es

I want to create a Dialogflow webhook that responds to the user slowly, so it more feels like someone is on the other end and takes a few seconds to reply.
I'm using the built-in code editor, and can create an Intent handler (see code), but I just don't know how to get it to reply slower.
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
function welcome (agent) {
agent.add(`I'm replying too quickly!`);
}
function fallback (agent) {
agent.add(`I didn't understand`);
agent.add(`I'm sorry, can you try again?`);
}
// Run the proper function handler based on the matched Dialogflow intent name
let intentMap = new Map();
intentMap.set('Default Welcome Intent', welcome);
intentMap.set('Default Fallback Intent', fallback);
agent.handleRequest(intentMap);
});

Best way to handle this is to add delay in the UI code.
Keep the Dialogflow Intent as it is, and once the bot response is received on the frontend, show it with a delay.
Below is an example of how we are handling it at Kommunicate
Dialogflow response comes without any delay, then on Javascript code, we show a typing indicator animation, add some delay using Javascript before displaying it.

Needing to reply slower is rarely a desired thing, to be honest. But the easiest way to do so is to use setTimeout() and delay for a little bit. (Don't delay too long - more than 5 or 10 seconds and Dialogflow will timeout.)
The catch with using setTimeout(), however, is that the handler will need to return a Promise. So you'll need to wrap the call to setTimeout() and the agent.add() in a Promise handler. A function that does this might look something like:
function respondSlowly( agent, msg, ms ){
return new Promise( resolve => {
setTimeout( () => {
agent.add( msg );
resolve();
}, ms );
});
}
You would then call this from your handler, providing the agent, the message, and how many milliseconds to wait to reply:
function welcome( agent ){
return respondSlowly( agent, `Hi there, slowly`, 2000 ); // Wait 2 seconds to reply
}

Related

How to map multiple intents to one handler in dialogflow-fulfillment?

There are multiple intents in my bot that can use a single handler.
In v1, this was done by
function handler (agent){
//handler code
}
let intentMap = new Map();
intentMap.set('Intent1', handler);
intentMap.set('Intent2', handler);
How can the same be achieved in v2?
Thanks in advance
I found one way to do this
app.intent('Intent1', (conv) => {
//handler code
});
app.intent('Intent2', 'Intent1');

conv.data lost in followup intent in DialogFlow

I'm using node.js 8 runtime in my Google Cloud function, attached to my DialogFlow app (V2 API).
I can use conv.data to store temporary data within the current conversation. Unfortunately, conv.data does not seem to retain data after a followup intent.
For example, in my intent the following code:
conv.data.result = "Hello!";
console.log("[DEBUG] conv.data.result = "+conv.data.result);
conv.followup("customEvent1");
produces the following log:
[DEBUG] conv.data.result = Hello!
This is my followup intent:
app.intent('CUSTOM_EVENT_INTENT', (conv) => {
console.log("[DEBUG] - CUSTOM_EVENT_INTENT");
console.log("[DEBUG] - conv.data.result = "+conv.data.result);
if(!conv.data.result) {
console.log("[DEBUG] - I give up");
conv.close("Nessuna risposta");
}
else conv.ask(conv.data.result);
});
which produces the following log:
[DEBUG] - conv.data.result = undefined
[DEBUG] - I give up
Looks like I'm missing something very important in followup intents...
Thanks,
Roberto
I would suggest using context instead of data. Context has few more advantages over data objects. It can be accessible in multiple dialog and intents.
function getParamFromContext(key){
// get the value set earlier in the context
let globalContext = agent.context.get('global_main_context');
return globalContext.parameters[key];
}
function updateGlobalContext(agent, key, value) {
// create a global method to maintain all required data within conversation
let globalContext = agent.context.get('global_main_context');
let param = globalContext ? globalContext.parameters : {};
param[key] = value;
agent.context.set({
name: 'global_main_context',
lifespan: 5,
parameters: param
});
}
Call the method within a conversation with agent object.
in app.js
intentMap.set("Size Intent", setSize);
function setSize(){
let size = agent.parameters.size;
updateShippingObjectContext(agent, 'size', size);
}
I think you're talking about Followup Events rather than Followup Intents. Followup Intents are set as Intents that may be triggered after an Intent has by the user doing some action - this is done by setting a Context. Followup Events are set during fulfillment and are intended to trigger an Intent that has this Event set.
In most cases, you don't need to use Followup Events.
Instead - just call a function that does the processing and replies how you want it to do. There is nothing saying that your Handler function has to do everything itself - it can call a function just like any other function, and can call it with parameters.
So it is perfectly reasonable to have Intent Handlers like
app.intent('intent.one', (conv) => {
reply( conv, "Hello!" );
});
app.intent('intent.two', (conv) => {
reply( conv, "How are you?" );
});
app.intent('intent.quit', (conv) => {
reply( conv );
});
function reply( conv, msg ){
if( !msg ){
conv.close( "I give up!" );
} else {
cov.ask( msg );
}
}
That still doesn't explain why it didn't work.
What you are "missing" with how you use Followup Events is that using conv.followup() does not send anything that you may have sent back to Dialogflow when it redirects to the Event. As the documentation says:
Triggers an intent of your choosing by sending a followup event from the webhook. [...] Dialogflow will not pass anything back to Google Assistant, therefore Google Assistant specific information, most notably conv.user.storage, is ignored.
What you can do, however, is send parameters to the new Intent that is detected from the Event. Something like this might work:
const params = {
result: "Hello!"
};
conv.followup("customEvent1", params);
and then in the handler for the event's Intent:
app.intent('CUSTOM_EVENT_INTENT', (conv) => {
console.log("[DEBUG] - CUSTOM_EVENT_INTENT");
console.log("[DEBUG] - conv.parameters.result = "+conv.parameters.result);
if(!conv.parameters.result) {
console.log("[DEBUG] - I give up");
conv.close("Nessuna risposta");
}
else conv.ask(conv.parameters.result);
});

Dialogflow : Provide Response after delay

I have a requirement to trigger an event from webhook which calls follow-up intent after 20 seconds delay.
I am trying to trigger custom event from setTimeOut within promise so that it calls desired intent.
function payment(agent){
const delay = t => new Promise(resolve => setTimeout(resolve, t));
return delay(5000).then(() => {console.log('Hello');
agent.setFollowupEvent('payment_completed');
});
}
console.log gets executed but event is not triggered.Please help.

Triggering the fulfillment webhook asynchronously from an intent?

I have some intents that need to trigger the fulfillment webhook and don't care about the response. The webhook takes longer than the timeout to respond so I'd like the intent to simply respond with "Thanks for chatting" and then close the conversation while actually triggering the webhook.
Feels easy but I'm missing something. Also I'm new to the dialogflow stuff.
I can do this in any language, but here's an example in Javascript:
fdk.handle(function (input) {
// Some code here that takes 20 seconds.
return {'fulfillmentText': 'i can respond but I will never make it here.'}
});
EDIT 1 - Trying async
When I use an async function, the POST request never happens. So in the following code:
fdk.handle(function (input) {
callFlow(input);
return { 'fulfillmentText': 'here is the response from the webhook!!' }
});
async function callFlow(input) {
console.log("input is --> " + input)
var url = "some_url"
console.log("Requesting " + url)
request(url, { json: true, headers: {'Access-Control-Allow-Origin' : '*'} }, (err, res, body) => {
if (err) { return console.log(err); }
console.log("body is...")
console.log(body)
});
}
I see in the logs the two console.log outputs but nothing from the request. And the request doesn't seem to happen either because I don't see it at my endpoint.
SOLUTION
Thanks Prisoner for the tip. Seems like I needed to return the fulfillment JSON back through the callFlow() and handle() functions. Now Google Home doesn't timeout and both the HTTP call and response are generated.
const fdk = require('#fnproject/fdk');
const request = require('request');
fdk.handle(function (input) {
return callFlow(input);
});
async function callFlow(input) {
var searchPhrase = input || "cats"
var url = "some url"
return new Promise((resolve, reject) => {
request.post(url, {
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: searchPhrase
},
function (err, resp, body) {
if (err) { return console.log(err) }
r = { 'fulfillmentText': `OK I've triggered the flow function with search term ${searchPhrase}` }
resolve(r)
}
);
});
}
You cannot trigger the fulfillment asynchronously. In a conversational model, it is expected that the fulfillment will perform some logic that determines the response.
You can, however, perform an asynchronous operation in the fulfillment that does not complete before you return the result.
If you are using a sufficiently modern version of node (version 8 and up), you can do this by declaring a function as an async function, but not calling it with the await keyword. (If you did call it with await, it would wait for the asynchronous operation to complete before continuing.)
So something like this should work, given your example:
async function doSomethingLong(){
// This takes 20 seconds
}
fdk.handle(function (input) {
doSomethingLong();
return {'fulfillmentText': 'This might respond before doSomethingLong finishes.'}
});
Update 1 based on your code example.
It seems odd that you report that the call to request doesn't appear to be done at all, but there are some odd things about it that may be causing it.
First, request itself isn't an async function. It is using a callback model and async functions don't just automatically wait for those callbacks to be called. So your callFlow() function calls console.log() a couple of times, calls request() and returns before the callbacks are called back.
You probably should replace request with something like the request-promise-native package and await the Promise that you get from the call. This makes callFlow() truly asynchronous (and you can log when it finishes the call).
Second, I'd point out that the code you showed doesn't do a POST operation. It does a GET by default. If you, or the API you're calling, expect a POST, that may be the source of the error. However, I would have expected the err parameter to be populated, and your code does look like it checks for, and logs, this.
The one unknown in the whole setup, for me, is that I don't know how fdk handles async functions, and my cursory reading of the documentation hasn't educated me. I've done this with other frameworks, and this isn't a problem, but I don't know if the fdk handler times out or does other things to kill a call once it sends a reply.

How to get current intent's name in Dialogflow fulfillment?

I want to get the name of the current intent in the fulfillment so I can deal with different response depending on different intent i'm at. But I cannot find a function for it.
function getDateAndTime(agent) {
date = agent.parameters.date;
time = agent.parameters.time;
// Is there any function like this to help me get current intent's name?
const intent = agent.getIntent();
}
// I have two intents are calling the same function getDateAndTime()
intentMap.set('Start Booking - get date and time', getDateAndTime);
intentMap.set('Start Cancelling - get date and time', getDateAndTime);
There is nothing magical or special about using the intentMap or creating a single Intent Handler per intent. All the handleRequest() function does is look at action.intent to get the Intent name, get the handler with that name from the map, call it, and possibly dealing with the Promise that it returns.
But if you're going to violate the convention, you should have a very good reason for doing so. Having a single Intent Handler per Intent makes it very clear what code is being executed for each matched Intent, and that makes your code easier to maintain.
It looks like your reason for wanting to do this is because there is significant duplicate code between the two handlers. In your example, this is getting the date and time parameters, but it could be many more things as well.
If this is true, do what programmers have been doing for decades: push these tasks to a function that can be called from each handler. So your examples might look something like this:
function getParameters( agent ){
return {
date: agent.parameters.date,
time: agent.parameters.time
}
}
function bookingHandler( agent ){
const {date, time} = getParameters( agent );
// Then do the stuff that uses the date and time to book the appointment
// and send an appropriate reply
}
function cancelHandler( agent ){
const {date, time} = getParameters( agent );
// Similarly, cancel things and reply as appropriate
}
intentMap.set( 'Start Booking', bookingHandler );
intentMap.set( 'Cancel Booking', cancelHandler );
request.body.queryResult.intent.displayName will give the the intent name.
'use strict';
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
function getDateAndTime(agent) {
// here you will get intent name
const intent = request.body.queryResult.intent.displayName;
if (intent == 'Start Booking - get date and time') {
agent.add('booking intent');
} else if (intent == 'Start Cancelling - get date and time'){
agent.add('cancelling intent');
}
}
let intentMap = new Map();
intentMap.set('Start Booking - get date and time', getDateAndTime);
intentMap.set('Start Cancelling - get date and time', getDateAndTime);
agent.handleRequest(intentMap);
});
But it would made more sense if you use two different functions in intentMap.set
you can try using "agent.intent" but it doesn't make sense to use the same function for two different intents.

Resources