Writing better code for Dialogflow Webhooks - node.js

I'm currently calling a fulfillment webhook with dialogflow in my node backend, performing crud operations on a firestore db. Is there a better, cleaner way to write these?
My code seems very poorly written but it works. I am striving to write cleaner more readable code so I'm looking for someone to give me some pointers on how to write better API calls with webhooks.
//DATABASE API CALLS HERE!//
case "FAV_COLOR":
agent.handleRequest(agent => {
return new Promise(() => {
async function writeToDb() {
// Get parameter from Dialogflow with the string to add to the database doc
const databaseEntry = agent.parameters.color;
// Get the database collection 'user' and document 'color' and store
// the document {entry: "<value of database entry>"} in the 'color' document
const dialogflowAgentRef = db.collection("user").doc("color");
try {
await db.runTransaction(transaction => {
transaction.set(dialogflowAgentRef, {
entry: databaseEntry
});
return Promise.resolve("Write complete");
});
agent.add(
`Wrote "${databaseEntry}" to the Firestore database.`
);
} catch (e) {
agent.add(
`Failed to write "${databaseEntry}" to the Firestore database.`
);
}
}
writeToDb();
});
});
break;
default:
console.log("ITS BROKEN");
It's currently inside a switch statement because I want to trigger different fulfillments based on actions. Both agent.add statements don't trigger at all.
Also, if someone could throw in some tips about debugging these I would really appreciate it. I've just been deploying the functions, adding a console.log(JSON.stringify()); and then checking in the firebase console functions section for errors. Seems incredibly inefficient.
Thanks for taking the time to answer :)
Jacks

Your index.js
const functions = require('firebase-functions');
const { WebhookClient } = require('dialogflow-fulfillment');
const welcome = require('./welcome')
const fallback = require('./fallback')
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
let intentMap = new Map();
intentMap.set('Default Welcome Intent', welcome);
intentMap.set('Default Fallback Intent', fallback);
agent.handleRequest(intentMap);
});
You can split files like welcome, fallback
=> welcome.js
const welcome = (agent) => {
agent.add(`Welcome to my agent!`);
}
module.exports = welcome
=> fallback.js
const fallback = (agent) => {
agent.add(`I didn't understand`);
agent.add(`I'm sorry, can you try again?`);
}
module.exports = fallback
You can use same method with thie Example

Related

Is there any way to read and update data in Firestore using Twilio Functions?

I am trying to read data from Firestore using the following code. I have added the firebase dependency in the dependencies file. But nothing seems to run except the template code at the end. I have also set the read rules for firestore to true for checking. I'm not even sure if Twilio can be used for this.
var firebase = require('firebase');
exports.handler = function(context, event, callback) {
var firebaseConfig = {
apiKey: "[API_KEY]",
authDomain: "[AUTH_DOMAIN]",
projectId: "[PROJECT_ID]",
storageBucket: "[STORAGE_BUCKET]",
messagingSenderId: "[MESSAGING_SENDER_ID]",
appId: "[APP_ID]"
};
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
console.log('Initialized Firebase app');
}else {
firebase.app();
console.log('Firebase initialized');
}
try{
const userRef = firebase.db.collection('users').doc('123');
const doc = userRef.get();
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
} catch(e) {
console.log(e);
}
// Here's an example of setting up some TWiML to respond to with this function
let twiml = new Twilio.twiml.VoiceResponse();
twiml.say('Hello World');
let variable = 'welcome!';
// You can log with console.log
console.log('error', variable);
// This callback is what is returned in response to this function being invoked.
// It's really important! E.g. you might respond with TWiML here for a voice or SMS response.
// Or you might return JSON data to a studio flow. Don't forget it!
return callback(null, twiml);
};
These are the dependencies added to the environment
Twilio developer evangelist here.
I think you have two issues with your code and they are both related to the asynchronous nature of JavaScript.
You are not treating any of the Firebase functions as asynchronous. It turns out that most are synchronous, but when you call .get on the userRef that is an asynchronous call. Since you are working with try/catch we can fix this quickly by defining the whole Twilio Function as an async function.
exports.handler = async function(context, event, callback) {
You can then use await before userRef.get() and things should start to work.
const doc = await userRef.get();
The second issue is that you were making that asynchronous call and not waiting for the response before you eventually called callback. Once you call the callback function the rest of the function execution is terminated. So while that asynchronous call to userRef.get() was actually made, it was cancelled once the function reached the callback because you weren't waiting for the result.
In both these cases, adding the async and await should sort out the issue and your function should run as expected.
Here's the whole function with the addition of async and await:
var firebase = require('firebase');
exports.handler = async function(context, event, callback) {
var firebaseConfig = {
apiKey: "[API_KEY]",
authDomain: "[AUTH_DOMAIN]",
projectId: "[PROJECT_ID]",
storageBucket: "[STORAGE_BUCKET]",
messagingSenderId: "[MESSAGING_SENDER_ID]",
appId: "[APP_ID]"
};
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
console.log('Initialized Firebase app');
}else {
firebase.app();
console.log('Firebase initialized');
}
try{
const userRef = firebase.db.collection('users').doc('123');
const doc = await userRef.get();
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
} catch(e) {
console.log(e);
}
// Here's an example of setting up some TWiML to respond to with this function
let twiml = new Twilio.twiml.VoiceResponse();
twiml.say('Hello World');
let variable = 'welcome!';
// You can log with console.log
console.log('error', variable);
// This callback is what is returned in response to this function being invoked.
// It's really important! E.g. you might respond with TWiML here for a voice or SMS response.
// Or you might return JSON data to a studio flow. Don't forget it!
return callback(null, twiml);
};

botframework v4 proactive messages not going through activityhandler event handlers

I Have a notification dialog to be invoked proactively...
e.g.
Bot: Hi, you have an event scheduled in next 15 mts... blah blah
Bot: would you want me to email the details?
User input: yes/no
Bot: Great!
This is a simple waterfall dialog...
step1 inform and prompt confirm.
step2. process user input..
Now this dialog is initiated proactively .
step 1 works.
However the dialogContext / dialogStack is not getting saved and when user says yes it is going to main dailog and not this proactive dialog which should be on top of stack.
Basically, none of the activityHandler methods like onDialog event on the activityHandler are getting invoked for proactive dialog.
Question is how to have the messages from proactive Dialog go through activityHandler methods so that the dialogStack is persisted ?
I use nodejs.
Updating with Code sample below
// middleware
const { ActivityTypes } = require('botbuilder');
class MyMiddleware {
async onTurn(context, next) {
await context.onSendActivities(async (context, activities, nextSend) => {
console.log(`messages: ${activities.map( a => a.text).join(',')}`)
return await nextSend();
});
// By calling next() you ensure that the next Middleware is run.
return await next();
};
}
module.exports.MyMiddleware = MyMiddleware;
// main bot.
const { ActivityHandler, TurnContext } = require('botbuilder');
class ProactiveBot extends ActivityHandler {
constructor(conversationReferences) {
super();
this.conversationReferences = conversationReferences;
this.onConversationUpdate(async (context, next) => {
this.addConversationReference(context.activity);
await next();
});
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let cnt = 0; cnt < membersAdded.length; cnt++) {
if (membersAdded[cnt].id !== context.activity.recipient.id) {
const welcomeMessage = 'Welcome to the Proactive Bot sample. Navigate to http://localhost:3978/api/notify to proactively message everyone who has previously messaged this bot.';
await context.sendActivity(welcomeMessage);
}
}
await next();
});
this.onMessage(async (context, next) => {
this.addConversationReference(context.activity);
await context.sendActivity(`You sent '${ context.activity.text }'`);
await next();
});
this.onDialog(async (context, next) =>{
console.log(`I am called`)
})
}
addConversationReference(activity) {
const conversationReference = TurnContext.getConversationReference(activity);
this.conversationReferences[conversationReference.conversation.id] = conversationReference;
}
}
module.exports.ProactiveBot = ProactiveBot;
// index
const bot = new ProactiveBot(conversationReferences);
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
// Route to main dialog.
await bot.run(context);
});
});
server.get('/api/notify', async (req, res) => {
for (const conversationReference of Object.values(conversationReferences)) {
await adapter.continueConversation(conversationReference, async turnContext => {
await turnContext.sendActivity('proactive hello');
});
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.write('<html><body><h1>Proactive messages have been sent.</h1></body></html>');
res.end();
});
When I call notify api I expect the onDialog event to be called... and print "I am called". But that doest not get printed to console.
It looks like you do not have the following bit of code in your "mainBot.js" file. Adding it should solve your problem and not require you to save state after every step. I have a proactive dialog that presents the user with a "yes/no" option, as well. However, I do not have to save with every step in order to capture the user's response.
this.onDialog(async (context, next) => {
console.log('Dialog detected');
// Save any state changes.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
await next();
});
That being said, I have a component dialog that is listening for the user's choice sending an appropriate response. The proactive dialog is not being captured but the response context is. Because the "mainDailog.js" extends the component dialog the context is added to the stack which is then processed via the this.onDialog() method in "mainBot.js". Thus, state is saved. Check out this SO response I posted recently that demo's this setup which includes the above code but is not displayed. In that case, the user also wanted a timer built into the process which you can ignore.
Hope of help!

Google Actions SDK second request in conversation is failing with "TypeError: standard is not a function "

I am building a Google Action using Actions SDK V2 library.
I am able to make the conversation work when I am using plain conv.ask and conv.close methods. But the moment I add async methods in to the processing chain, I am facing issues. I am using
"actions-on-google": "^2.4.1",
"firebase-admin": "^6.0.0",
"firebase-functions": "^2.1.0",
Exactly after every 3 failed attempts, the 4th attempt gets through. Looks like the previous request is hogging up my firebase cloud function!
First request to the fulfillment works just fine and the subsequent ones fail with "TypeError: standard is not a function at /srv/node_modules/actions-on-google/dist/framework/express.js"
severity: "ERROR"
textPayload: "TypeError: standard is not a function
at /srv/node_modules/actions-on-google/dist/framework/express.js:27:13
at omni (/srv/node_modules/actions-on-google/dist/assistant.js:44:53)
at cloudFunction (/srv/node_modules/firebase-functions/lib/providers/https.js:57:9)
at /worker/worker.js:700:7
at /worker/worker.js:684:9
at _combinedTickCallback (internal/process/next_tick.js:131:7)
at process._tickDomainCallback (internal/process/next_tick.js:218:9)"
I understand that when I've asynchronous functions in the processing chain, I should return Promise to the Intent handler. And I am doing that. But still the subsequent request fails.
const app = actionssdk();
// ... app code here
app.middleware((conv) => {
console.log('Conversation in Middleware:', conv);
conv.hasScreen =
conv.surface.capabilities.has('actions.capability.SCREEN_OUTPUT');
conv.hasAudioPlayback =
conv.surface.capabilities.has('actions.capability.AUDIO_OUTPUT');
});
// Welcome Intent
app.intent('actions.intent.MAIN', (conv, input) => {
// conv.ask('How are you');
console.log('Conversation: ' + JSON.stringify(conv), conv);
console.log('actions.intent.MAIN', input)
return new Promise((resolve, reject) => {
processIntents({conv: conv, input:input}).then(r => {
console.log('Passing response from the handler!', r);
for(let a of r) {
conv.ask(a);
}
resolve();
}).catch(e => reject(e));
});
});
// React to a text intent
app.intent('actions.intent.TEXT', (conv, input) => {
console.log('actions.intent.TEXT', input)
// conv.ask('How are you');
console.log('Conversation: ' + JSON.stringify(conv), conv);
console.log('actions.intent.TEXT', input)
return new Promise((resolve, reject) => {
processIntents({conv: conv, input:input}).then(r => {
console.log('Passing response from the handler!', r);
for(let a of r) {
conv.ask(a);
}
resolve();
}).catch(e => reject(e));
});
});
// React to list or carousel selection
app.intent('actions.intent.OPTION', (conv, params, option) => {
// conv.ask('How are you');
console.log('actions.intent.OPTION', {input: conv.input, option: option, params:params})
console.log('Conversation: ' + JSON.stringify(conv), conv);
console.log('actions.intent.OPTION', option)
return new Promise((resolve, reject) => {
processIntents({conv: conv, params: params, option: option}).then(r => {
console.log('Passing response from the handler!', r);
for(let a of r) {
conv.ask(a);
}
resolve();
}).catch(e => reject(e));
});
});
app.intent('', (conv) => {
// conv.ask('How are you');
console.log('Empty Intent for direct WEB requests / ALEXA requests', conv);
let input = ((conv.request.originalRequest ||{}).result ||{}).resolvedQuery;
return new Promise((resolve, reject) => {
processIntents({conv: conv, input: input}).then(r => {
//TODO: send raw HTTP Response
//TODO: Yet to figure out how this can be done#!
resolve();
}).catch(e => reject(e));
});
});
const processIntents = (args) => {
let conv = args["conv"];
let params = args["params"];
let option = args["option"];
let input = args["input"];
// Async Function here that responds with a Promise
// Based on the request, it will send either array of responses to use in conv.ask calls
// OR send response JSON body that can be sent to Amazon Alexa / WEB requests
return Promise.resolve(response);
}
exports.apiaifns = functions.https.onRequest(app);
I am sending an array of conversation responses from the processIntents method and iterating through the array while responding.
I've tried many possible permutations and combinations of this, but still the subsequent request fails.
Update : Testing with Google Cloud Functions instead of GCF for Firebase
Updated my app code to make it an express app and deployed to GCFs with the same name. Now the situatio is worse. Only first request after deployment of the Cloud Function is successful and all subsequent requests fail with the same error as before!
Here is my update to the function:
const express = require('express')
const bodyParser = require('body-parser')
var morgan = require('morgan')
.
.
.
const expressApp = express().use(bodyParser.json())
expressApp.use(morgan('dev'))
expressApp.post('/fulfillment', app)
exports.apiaifns = expressApp;
I've also changed the actio invocation URL to "https://us-central1-my-project.cloudfunctions.net/apiaifns/fulfillment"
This is litarally driving me crazzy, and I couldn't find a reason for the issue! Kindly help.

ReferenceError: conv is not defined in actions-on-google

I want to implement Suggestions chips in my Dialog flow (to be use in Google Assistant)..But I am getting this error
"ReferenceError: conv is not defined"
which i didn't understand.I have go through the official docs but what am i missing? I have also added actions_intent_OPTION in his Event
following is my code
const functions = require('firebase-functions');
const {actionssdk} = require('actions-on-google');
const app = actionssdk({debug: true});
var admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
var firestore = admin.firestore();
exports.webhook = functions.https.onRequest((request, response) => {
switch (request.body.result.action) {
case 'countitem':
firestore.collection('orders').get()
.then((querySnapshot) => {
var orders = [];
querySnapshot.forEach((doc) => { orders.push(doc.data()) });
// now orders have something like this [ {...}, {...}, {...} ]
response.send({
speech: `you have ${orders.length} orders11, would you like to see them? (yes/no)`
});
})
.catch((err) => {
console.log('Error getting documents', err);
response.send({
speech: "something went wrong when reading from database"
})
})
conv.ask(new Suggestions('Suggestion Chips'));
conv.ask(new Suggestions(['suggestion 1', 'suggestion 2']));
break;
default:
response.send({
speech: "no action matched in webhook"
})
}
});
The issue is that conv isn't defined. Typically, if you're using the actions-on-google library, conv is passed to your fulfillment function and contains methods you can use to set replies and so forth.
It looks like you're handling everything yourself and generating the JSON response manually. If so, you should consult the guide for using JSON as part of your webhook and the repository of JSON examples.

Unhandled Rejection: Headers cant be set after they are sent

I am creating a chatbot in Dialogflow. I am trying to add the data to the database when its throwing an error of Unhandled Rejection.
This is my index.js file.
'use strict';
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
//const {Card, Suggestion} = require('dialogflow-fulfillment');
var admin = require('firebase-admin');
//require("firebase/firestore");
admin.initializeApp(functions.config().firebase);
var firestore = admin.firestore();
//var db = firebase.firestore();
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
var addRef = firestore.collection('Admission');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
console.log("request.body.queryResult.parameters: ", request.body.queryResult.parameters);
let params = request.body.queryResult.parameters;
/* function welcome (agent) {
agent.add(`Welcome to my agent!`);
} */
/* function fallback (agent) {
agent.add(`I didn't understand`);
agent.add(`I'm sorry, can you try again?`);
} */
let responseJson ={};
function yourFunctionHandler(agent) {
var docRef = firestore.collection('users');
name = request.body.queryResult.parameters['myname'];
coll = request.body.queryResult.parameters['college_name'];
name = name.charAt(0).toUpperCase() + name.slice(1);
let balanceresponse = {};
console.log(name);
return docRef.add({
myname: name,
college: coll
})
.then((querySnapshot)=>{
balanceresponse = {
"fulfillmentText": 'Sure '+name+', Do you want to know about Admissions, Fees, Graduates and PG, Contact information, Media or Testimonials?'
}
console.log('before response.send()');
response.send(balanceresponse);
/* console.log('before response.send()');
response.send({
fulfillmentText:
'Sure '+name+', Do you want to know about Admissions, Fees, Graduates and PG, Contact information, Media or Testimonials?'
//response.json(responseJson);
}); */
console.log('after response.send()');
return 1;
})
.catch(error => {
response.send({
fulfillmentText:
'Something went wrong with the database'
});
});
}
function AdmissionHandler(agent) {
console.log("inside Admission Handler");
let balanceresponse = {};
let Question = request.body.queryResult.parameters['question'];
addRef.where("Question", "==", Question)
.get().then((querySnapshot)=>{
if (querySnapshot) {
console.log("Document data:");
const tips = querySnapshot.docs;
const tipIndex = Math.floor(Math.random() * tips.length);
const tip = tips[0];
balanceresponse = {
"fulfillmentText": ' Anything else you wanna know?'
}
console.log('before response.send()');
response.send(balanceresponse);
/* response.send({
fulfillmentText:
//firestore.collection(addRef.Answer)+
' Anything else you wanna know?'
}); */
return 1;
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
return 1;
})
.catch(function(error) {
console.log("Error getting document:", error);
});
}
// 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);
intentMap.set('GetName', yourFunctionHandler);
intentMap.set('AdmissionCustom', AdmissionHandler);
agent.handleRequest(intentMap);
});
This is the error I receive:
I have seen few similar questions here but none of them are answered. Can anyone please help? I have been stuck in this for over a week already.
The problem is that the yourFunctionHandler(agent) function is doing things asynchronously, but isn't returning a Promise. Instead, it returns nothing, so the processing returns immediately without having a message sent.
Since it looks like myDoc.add() returns a Promise, this is easy to handle by making that return myDoc.add(...).then(...) and so forth. It might look something like this:
function yourFunctionHandler(agent) {
return docRef.add({
myname: name,
college: coll
})
.then(()=>{
response.send({
fulfillmentText:
'Sure '+name+', Do you want to know about Admissions, Fees, Graduates and PG, Contact information, Media or Testimonials?'
});
return 1;
})
.catch(error => {
//console.log('érror',e);
response.send({
fulfillmentText:
'Something went wrong with the database'
});
});
}
Additionally, you're mixing handling the response yourself (by calling response.send()) and using the Dialogflow agent.handleRequest(), which will create a response for you.
You should either use the Dialogflow methods to generate the reply with something like
agent.add("No such document found.");
or use the values in the JSON yourself to determine which handler to call with something like
const intentName = request.body.queryResult.intent.name;
const handler = intentMap[intentName];
handler();
(You may need to vary this. It looks like from your code you're using Dialogflow v1, which I've reflected, and the path for the intent name changes for v2. You also should check for the handler not existing, may want to send parameters, etc.)

Resources