DialogFlow V2 Webhook - Expects Speech responses Immediately and not after async requests - node.js

I have a DialogFlow V2 node.js webhook.
I have an intent that is called with a webhook action:
const { WebhookClient } = require('dialogflow-fulfillment');
const app = new WebhookClient({request: req, response: res});
function exampleIntent(app) {
app.add("Speak This Out on Google Home!"); // this speaks out fine. no error.
}
Now, if I have an async request which finishes successfully, and I do app.add in the success block like this:
function exampleIntent(app) {
myClient.someAsyncCall(function(result, err) {
app.add("This will not be spoken out"); // no dice :(
}
// app.add("but here it works... so it expects it immediately");
}
... then Dialog Flow does not wait for the speech to be returned. I get the error in the Response object:
"message": "Failed to parse Dialogflow response into AppResponse, exception thrown with message: Empty speech response",
How can I make DialogFlow V2 wait for the Webhook's Async operations to complete instead expecting a speech response immediately?
NOTE: This problem only started happening in V2. In V1, app.ask worked fine at the tail-end of async calls.
exampleIntent is being called by the main mapper of the application like this:
let actionMap = new Map();
actionMap.set("my V2 intent name", exampleIntent);
app.handleRequest(actionMap);
And my async request inside myClient.someAsyncCall is using Promises:
exports.someAsyncCall = function someAsyncCall(callback) {
var apigClient = getAWSClient(); // uses aws-api-gateway-client
apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
.then(function(result){
var result = result.data;
var message = result['message'];
console.log('SUCCESS: ' + message);
callback(message, null); // this succeeds and calls back fine.
}).catch( function(error){
console.log('ERROR: ' + error);
callback(error, null);
});
};

The reason it worked in V1 is that ask() would actually send the request.
With V2, you can call add() multiple times to send everything to the user in the same reply. So it needs to know when it should send the message. It does this as part of dealing with the response from your handler.
If your handler is synchronous, it sends the reply immediately.
If your handler is asynchronous, however, it assumes that you are returning a Promise and waits till that Promise resolves before sending the reply. So to deal with your async call, you need to return a Promise.
Since your call is using Promises already, then you're in very good shape! The important part is that you also return a Promise and work with it. So something like this might be your async call (which returns a Promise):
exports.someAsyncCall = function someAsyncCall() {
var apigClient = getAWSClient(); // uses aws-api-gateway-client
return apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
.then(function(result){
var result = result.data;
var message = result['message'];
console.log('SUCCESS: ' + message);
return Promise.resolve( message );
}).catch( function(error){
console.log('ERROR: ' + error);
return Promise.reject( error );
});
};
and then your Intent handler would be something like
function exampleIntent(app) {
return myClient.someAsyncCall()
.then( function( message ){
app.add("You should hear this message": message);
return Promise.resolve();
})
.catch( function( err ){
app.add("Uh oh, something happened.");
return Promise.resolve(); // Don't reject again, or it might not send the reply
})
}

Related

Can't pass callback function to SocketIO in typescript

Stack: Node.js + Express + TypeScript + Socket.io
Problem: I cannot transfer the callback provided in the library using TypeScript
How do I call the callback correctly? The code below throws an error.
socket.on('method', async (params: any, callback: (res: any) => void) => {
// endpoint's logic
const result = await this._service.ServiceMethodAsync(params);
// acknowledgement
callback(result);
})
Error
TypeError: callback is not a function
If you are performing validation on the event that has triggered on the socket then I would say follow the example provided on their page here.
In the case you have provided above you would have:
socket.on('method', async (params, callback) => {
if (typeof callback !== "function") {
// event is not an acknowledgement, do something i.e. return
return;
}
const result = await this._service.ServiceMethodAsync(params);
// acknowledgement
callback(result);
})
Here is another resource on understanding Acknowledgements in SocketIO.
checking the client-side code and turns out the snippet mentioned above is just fine and there was a problem with emitting the method.
fixed client:
socket.emit('method', msg, (res) => {
console.log(res);
});

Make HTTP GET from fulfillment in Node.JS

I'm trying to make my own google action and I want to call an external api to get responses.
Here is my code:
const { conversation } = require('#assistant/conversation');
const functions = require('firebase-functions');
const app = conversation({debug:true});
const https = require('https');
app.handle('Tester', conv => {
// Implement your code here
conv.add("ok it works");
});
app.handle('Tester2', conv => {
// Implement your code here
let url = 'https://jsonplaceholder.typicode.com/users?_limit=2';
//const url = "https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22";
http_req(url).then((message)=> {
console.log(message[0].name);
conv.add(message[0].name);
//return Promise.resolve();
});
});
function http_req(url) {
return new Promise((resolve, reject) => {
https.get(url, function(resp) {
var json = "";
resp.on("data", function(chunk) {
//console.log("received JSON response: " + chunk);
json += chunk;
});
resp.on("end", function() {
let jsonData = JSON.parse(json);
console.log(jsonData[0].name);
resolve(jsonData);
});
}).on("error", (err) => {
reject("Error: " + err.message);
});
});
}
exports.ActionsOnGoogleFulfillment = functions.https.onRequest(app);
The logs:
Error text:
Error: Response has already been sent. Is this being used in an async call that was not returned as a promise to the intent handler?
The problem is that the assistant won't say the conv.add(message[0].name); (obviusly it has a value)
Thanks in advance!
Thanks to a reddit user
https://www.reddit.com/r/GoogleAssistantDev/comments/lia5r4/make_http_get_from_fulfillment_in_nodejs/gn23hi3?utm_source=share&utm_medium=web2x&context=3
This error messages tells you just about all you need to know! Your
call to con.add() is indeed being used in an asynchronous call (the
callback chained to the Promise you created from http_req), and you
are indeed not returning that Promise.
Here's what's happening:
Google calls your 'Tester2' handler
You start an asynchronous HTTP request via http_req, encapsulated in a
Promise
Your function completes before the HTTP request does
Google sees that you are not returning anything from your handler and
assumes that you're done, so it sends the Response
The HTTP request finishes and its Promise resolves, calling your code
attached by the then() function
The simple solution here is to return the Promise created by your
http_req(...).then(...) code, so Google will know that you're not just
quite done, and it should wait for that Promise to resolve before
sending the Response.
If you can use async/await it becomes a bit clearer:
app.handle('Tester2', async conv => {
// Implement your code here
let url = 'https://jsonplaceholder.typicode.com/users?_limit=2';
//const url = "https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22";
const message = await http_req(url);
console.log(message[0].name);
conv.add(message[0].name);
});

Twilio programmable SMS not sending in deployed Lambda function

I am working on a Serverless AWS service that uses Twilio's programmable SMS to deliver text messages.
My setup is consistently delivering messages successfully when I run the stack locally (e.g. sls offline start), but in the deployed environment I seem to be unable to even call the method on the Twilio client.
Here's how the message delivery is set up:
const twilio = require('twilio');
const twilioClient = twilio(
process.env.TWILIO_SID,
process.env.TWILIO_TOKEN,
{
lazyLoading: true,
}
);
export function sendMessage(user, message) {
twilioClient.messages.create({
from: process.env.TWILIO_NUMBER,
to: user.phone,
body: message,
}, function(err, message) {
console.log('error', err);
console.log('message', message);
});
}
// And then usage in a Serverless Function Handler
function example(event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false;
// user is also determined here
sendMessage(user, 'This is a message');
return {
body: JSON.stringify({}),
statusCode: 200
};
}
Locally, running this works and I am able to see the output of the message log, with nothing on the error log. However, when deployed, running this yields nothing -- the method appears to not even get called (and I can verify in the Twilio logs that no API call was made), therefor no error or message logs are made in the callback.
In debugging I've tried the following:
I've logged all the environment variables (Twilio SSID, auth token, phone number), as well as the function arguments, and they all appear to be in place. I also checked out the Lambda function itself to make sure the environment variables exist.
I've examined my CloudWatch logs; no errors or exceptions are logged. Other than the Twilio method not getting called the Lambda function is executed without issue.
I've tried logging things like twilio and twilioClient.messages.create to make sure the client and function definition didn't get wiped out somehow.
I thought maybe it had to do with context.callbackWaitsForEmptyEventLoop so I changing it from false to true.
I have come up empty, I can't figure out why this would be working locally, but not when deployed.
Edit: per the Twilio client example, if you omit the callback function the method will return a Promise. I went ahead and tried to await the response of the method:
export function sendMessage(user, message) {
return twilioClient.messages.create({
from: process.env.TWILIO_NUMBER!,
to: user.phone,
body: message,
});
}
// Usage...
async function example(event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false;
try {
const message = await sendMessage(user, 'This is a message');
console.log('message', message)
} catch (error) {
console.log('error', error);
}
return {
body: JSON.stringify({}),
statusCode: 200
};
}
In this example the Lambda function is successful, but neither the message nor the error are logged.
I tried this and it works. I tried to make my code as similar to use, with a few changes.
const twilio = require('twilio');
const twilioClient = twilio(
process.env.TWILIO_SID,
process.env.TWILIO_TOKEN
);
let user = '+14075551212';
function sendMessage(user, message) {
return twilioClient.messages.create({
from: process.env.TWILIO_NUMBER,
to: user,
body: message,
});
}
exports.handler = async function(event, context, callback) {
try {
const message = await sendMessage(user, 'This is a message');
console.log('message', message);
callback(null, {result: 'success'});
} catch (error) {
console.log('error', error);
callback("error");
}
};

Keep variable between promises in a function?

On firebase function I need to get data from Paypal and do 4 things :
1. returns an empty HTTP 200 to them.
2. send the complete message back to PayPal using `HTTPS POST`.
3. get back "VERIFIED" message from Paypal.
4. *** write something to my Firebase database only here.
What I do now works but i am having a problem with (4).
exports.contentServer = functions.https.onRequest((request, response) => {
....
let options = {
method: 'POST',
uri: "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr",
body: verificationBody
};
// ** say 200 to paypal
response.status(200).end();
// ** send POST to paypal back using npm request-promise
return rp(options).then(body => {
if (body === "VERIFIED") {
//*** problem is here!
return admin.firestore().collection('Users').add({request.body}).then(writeResult => {return console.log("Request completed");});
}
return console.log("Request completed");
})
.catch(error => {
return console.log(error);
})
As you can see when I get final VERIFIED from Paypal I try to write to the db with admin.firestore().collection('Users')..
I get a warning on compile :
Avoid nesting promises
for the write line.
How and where should I put this write at that stage of the promise ?
I understand that this HTTPS Cloud Function is called from Paypal.
By doing response.status(200).end(); at the beginning of your HTTP Cloud Function you are terminating it, as explained in the doc:
Important: Make sure that all HTTP functions terminate properly. By
terminating functions correctly, you can avoid excessive charges from
functions that run for too long. Terminate HTTP functions with
res.redirect(), res.send(), or res.end().
This means that in most cases the rest of the code will not be executed at all or the function will be terminated in the middle of the asynchronous work (i.e. the rp() or the add() methods)
You should send the response to the caller only when all the asynchronous work is finished. The following should work:
exports.contentServer = functions.https.onRequest((request, response) => {
let options = {
method: 'POST',
uri: "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr",
body: verificationBody
};
// ** send POST to paypal back using npm request-promise
return rp(options)
.then(body => {
if (body === "VERIFIED") {
//*** problem is here!
return admin.firestore().collection('Users').add({ body: request.body });
} else {
console.log("Body is not verified");
throw new Error("Body is not verified");
}
})
.then(docReference => {
console.log("Request completed");
response.send({ result: 'ok' }); //Or any other object, or empty
})
.catch(error => {
console.log(error);
response.status(500).send(error);
});
});
I would suggest you watch the official Video Series on Cloud Functions from Doug Stevenson (https://firebase.google.com/docs/functions/video-series/) and in particular the first video on Promises titled "Learn JavaScript Promises (Pt.1) with HTTP Triggers in Cloud Functions".

How do I call a third party Rest API from Firebase function for Actions on Google

I am trying to call a rest API from Firebase function which servers as a fulfillment for Actions on Google.
I tried the following approach:
const { dialogflow } = require('actions-on-google');
const functions = require('firebase-functions');
const http = require('https');
const host = 'wwws.example.com';
const app = dialogflow({debug: true});
app.intent('my_intent_1', (conv, {param1}) => {
// Call the rate API
callApi(param1).then((output) => {
console.log(output);
conv.close(`I found ${output.length} items!`);
}).catch(() => {
conv.close('Error occurred while trying to get vehicles. Please try again later.');
});
});
function callApi (param1) {
return new Promise((resolve, reject) => {
// Create the path for the HTTP request to get the vehicle
let path = '/api/' + encodeURIComponent(param1);
console.log('API Request: ' + host + path);
// Make the HTTP request to get the vehicle
http.get({host: host, path: path}, (res) => {
let body = ''; // var to store the response chunks
res.on('data', (d) => { body += d; }); // store each response chunk
res.on('end', () => {
// After all the data has been received parse the JSON for desired data
let response = JSON.parse(body);
let output = {};
//copy required response attributes to output here
console.log(response.length.toString());
resolve(output);
});
res.on('error', (error) => {
console.log(`Error calling the API: ${error}`)
reject();
});
}); //http.get
}); //promise
}
exports.myFunction = functions.https.onRequest(app);
This is almost working. API is called and I get the data back. The problem is that without async/await, the function does not wait for the "callApi" to complete, and I get an error from Actions on Google that there was no response. After the error, I can see the console.log outputs in the Firebase log, so everything is working, it is just out of sync.
I tried using async/await but got an error which I think is because Firebase uses old version of node.js which does not support async.
How can I get around this?
Your function callApi returns a promise, but you don't return a promise in your intent handler. You should make sure you add the return so that the handler knows to wait for the response.
app.intent('my_intent_1', (conv, {param1}) => {
// Call the rate API
return callApi(param1).then((output) => {
console.log(output);
conv.close(`I found ${output.length} items!`);
}).catch(() => {
conv.close('Error occurred while trying to get vehicles. Please try again later.');
});
});

Resources