Autopilot and Functions collecting data he - node.js

I have a task in autopilot that collects data from a caller then calls a function using a redirect.
I cant seem to access the post variables. please assist.
so when run this I get the following error
Error - 82002
Error on Twilio Function response
produced by this line of code
var first_name = memory.twilio.collected_data.lead_qual.lead_qual_first;
remove that line and it works fine just no access to the collected data.
following are the dependencies I have included, the task code and the function.
looks like this..
Dependencies......>
lodash 4.17.11
twilio 3.29.2
fs 0.0.1-security
got 9.6.0
moment-timezone 0.5.14
moment 2.29.1
xmldom 0.1.27
twilio/runtime-handler 1.0.1
util 0.11.0
request 2.87.0
Task......>
{
"actions": [
{
"collect": {
"name": "lead_qual",
"questions": [
{
"question": "What is your first name?",
"name": "lead_qual_first",
"type": "Twilio.FIRST_NAME"
},
{
"question": "What is your last name?",
"name": "lead_qual_last",
"type": "Twilio.LAST_NAME"
},
{
"question": "If we are disconnected what is the best phone number to reach you on??",
"name": "lead_qual_phone",
"type": "Twilio.PHONE_NUMBER"
},
{
"question": "What is your date of birth?",
"name": "lead_qual_dob",
"type": "Twilio.DATE"
},
{
"question": "Are you currently covered by disability, yes or no?",
"name": "lead_qual_disability",
"type": "Twilio.YES_NO"
},
{
"question": "Do you have any form of federal medicare, yes or no?",
"name": "lead_qual_medicare",
"type": "Twilio.YES_NO"
},
{
"question": "Do you have medicaid or another state sponsored insurance, yes or no?",
"name": "lead_qual_medicaid",
"type": "Twilio.YES_NO"
},
{
"question": "Finally, Are you currently insured, yes or no?",
"name": "lead_qual_insured",
"type": "Twilio.YES_NO"
}
],
"on_complete": {
"redirect": {
"method": "POST",
"uri": "https://health-agent-3097.twil.io/Evaluate-Answers"
}
}
}
}
]
}
Function ......>
// This is your new function. To start, set the name and path on the left.
exports.handler = function(context, event, callback) {
// Require the component used to post.
const got = require("got");
// Time zone for EST to check times with.
let moment = require('moment-timezone');
const now = moment().tz('America/New_York');
// initialize the return object
var responseObject = {};
var first_name = memory.twilio.collected_data.lead_qual.lead_qual_first;
responseObject =
{
"actions":[
{
"say":"Force Override result"
},
{
"redirect": {
"method": "POST",
"uri": "task://goodbye"
}
}
]
}
// This callback is what is returned in response to this function being invoked.
callback(null, responseObject);}

Twilio developer evangelist here.
memory is not one of the arguments passed to a Twilio Function. You are passed event, context and callback. You are using the callback correctly and the context includes your environment variables.
The event object is what you need here though. The event includes all the parameters sent to the Function. According to the documentation on the Autopilot request you will be sent a Memory parameter. That Memory will be a JSON string that needs parsing. So instead, try accessing:
const memory = JSON.parse(event.Memory);
const firstName = memory.twilio.collected_data.lead_qual.lead_qual_first;
Let me know how you get on with that.

Related

Twilio autopilot send response as image to users whatsapp

I am building a bot so whenever a specific task is initiated I want to send an image to the user.
exports.handler = function(context, event, callback) {
let response = {
"actions": [
{
"show": {
"body": "Twilio Owls",
"images": [{
"label": "Original Owl",
"url": "https://demo.twilio.com/owl.png"
}]
}
}]
}
callback(null, response)
}
I tried using the above code inside my function and I have linked it to my Twilio autopilot task, It is working in the simulator, but when I am testing it on Whatsapp Image does not appear in the Whatsapp chat only body get displayed.
I tested and received the owl image. I noticed have a rogue {, maybe that is it?
exports.handler = function(context, event, callback) {
let response = {
"actions": [
{
"show": {
"body": "Twilio Owls",
"images": [{
"label": "Original Owl",
"url": "https://demo.twilio.com/owl.png"
}]
}
}]
}
callback(null, response)
};

How to get Session Entities to work as part of Dialogflow detect intent

I have an Entity that is supposed to be updated on a per-session basis with user-specific information. This had worked when I was using Dialogflow v1, and I thought it had worked with v2, but I'm now having significant problems with it.
I believe I am setting the Session Entity information correctly, but for the Intent that uses it, it only matches when a value from the Developer Entity is used.
How can I get it to use the Session Entity? Am I doing something wrong when updating it? Am I using the wrong Session ID? Is there a way I can better verify or test that I'm using the correct ID or that I'm updating the Entity correctly? Is this just a bug?
Documentation of everything follows.
The project is configured to use v2 and to allow for beta features, although I've tried this without the beta features as well.
There are only three Intents. A Fallback Intent to capture failures, a Welcome Intent that gets the welcome event, and the "entry" intent that is supposed to capture the entry code which should match the "code" Entity. All of them use a webhook for fulfillment.
The Fallback Intent
The Welcome Intent
The "entry" Intent
As shown in the "entry" Intent, it uses the "code" Entity, which is the only Developer Entity in the system
The code has most of the Dialogflow specific work in a separate module that uses the "dialogflow" module from npm to set the Session Entity. (Note this is different than the dialogflow-fulfillment module, which is used to handle fulfillment. I'm using the multivocal library for fulfillment, but that shouldn't matter.) (It also uses firebase functions to run on, but I don't think any of these are relevant.)
From the package.json:
"dependencies": {
"dialogflow": "^0.9.0",
"firebase-admin": "~7.0.0",
"firebase-functions": "^2.2.0",
"multivocal": "^0.11.1"
},
This is imported as dialogflow, specifying the API version to use:
const dialogflow = require('dialogflow').v2beta1;
The functions I show below call envToConfig(env) which takes the environment (a multivocal concept that just stores relevant information, including the Dialogflow parent and certificate information) and returns the configuration that needs to be passed to dialogflow.SessionEntityTypesClient( config ). Given no errors are thrown in further calls, it appears to work correctly.
The makeEntityType( name, entityMap ) function takes a map of values to be used for the entities in a SessionEntityType and returns an object that will be used to build a full SessionEntityType. The name provided here is the display name.
function makeEntityType( name, entityMap ){
let ret = {
displayName: name,
entities: []
};
Object.keys( entityMap ).map( key => {
let val = entityMap[key];
let entity = {
value: key,
synonyms: [key, ...val]
};
ret.entities.push( entity );
});
return ret;
}
exports.makeEntityType = makeEntityType;
The result from this is passed to setSessionEntity( env, entityType ) along with the multivocal environment, which contains some information we use in the session. It makes sure the name and entityOverrideMode are set correctly in the entityType and then tries to create it. I've tried using PATCH as well, and it behaves the same way. It also does a bunch of logging, that I'll show later when it runs to prove it actually works.
function setSessionEntity( env, entityType ){
const config = envToConfig( env );
const client = new dialogflow.SessionEntityTypesClient( config );
let parent = env.dialogflow.parent;
if( entityType.displayName && !entityType.name ){
entityType.name = `${parent}/entityTypes/${entityType.displayName}`;
}
if( !entityType.entityOverrideMode ){
entityType.entityOverrideMode = 'ENTITY_OVERRIDE_MODE_OVERRIDE';
}
console.log('setSessionEntity parent',parent);
const request = {
parent: parent,
sessionEntityType: entityType
};
console.log('setSessionEntity request',JSON.stringify(request,null,1));
return client.createSessionEntityType( request )
.then( create => {
console.log('setSessionEntity created',JSON.stringify(create,null,1));
return Promise.resolve( env );
})
.catch( err => {
console.error('setSessionEntity problem creating',err);
return Promise.resolve( env );
})
}
exports.setSessionEntity = setSessionEntity;
For debugging, I also have a function that lists the session entities:
function listSessionEntities( env ){
let parent = env.dialogflow && env.dialogflow.parent;
console.log('listSessionEntities parent', parent);
if( !parent ){
return Promise.resolve( env );
}
const config = envToConfig( env );
const client = new dialogflow.SessionEntityTypesClient( config );
const request = {
parent: parent
};
return client.listSessionEntityTypes(request)
.then( result => {
console.log('listSessionEntities', JSON.stringify(result,null,1));
})
.catch( err => {
console.log('listSessionEntities err', err);
})
.then( () => Promise.resolve( env ) );
}
exports.listSessionEntities = listSessionEntities;
The code that calls this imports it as Dialogflow:
const Dialogflow = require('./dialogflow');
As part of all the webhook calls, the listSessionEntities() function is called before any specific handler is:
function debugSessionEntities( env ){
return Dialogflow.listSessionEntities( env );
}
When the Welcome Intent is triggered, it sets the "code" Session Entity to have two new types that should override the type that was defined in the "code" Developer Entity:
function handleWelcome( env ){
const entityType = Dialogflow.makeEntityType('code',{
'alpha': [],
'bravo': []
});
return Dialogflow.setSessionEntity( env, entityType )
.then( env => Multivocal.handleDefault( env ) );
}
When I run this through the simulator, it doesn't pick up on the Session Entity Types that are set, but do still respond to the Developer Entity Type. (Using a real device works the same way.)
In the simulator, this is what it reports in the Request tab for the Welcome Intent:
{
"responseId": "55a9eb06-ce05-48f9-8a56-b993fa512aee",
"queryResult": {
"queryText": "GOOGLE_ASSISTANT_WELCOME",
"action": "multivocal.welcome",
"parameters": {},
"allRequiredParamsPresent": true,
"fulfillmentText": "Hello! How can I help you?",
"fulfillmentMessages": [
{
"text": {
"text": [
"Greetings! How can I assist?"
]
}
}
],
"outputContexts": [
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/google_assistant_welcome"
},
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/actions_capability_screen_output"
},
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/actions_capability_audio_output"
},
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/actions_capability_account_linking"
},
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/google_assistant_input_type_keyboard"
},
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/actions_capability_media_response_audio"
},
{
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/contexts/actions_capability_web_browser"
}
],
"intent": {
"name": "projects/session-test-XXXXX/agent/intents/ca79c951-4d75-4b2b-acd4-7dac2f81856e",
"displayName": "welcome"
},
"intentDetectionConfidence": 1,
"languageCode": "en-us"
},
"originalDetectIntentRequest": {
"source": "google",
"version": "2",
"payload": {
"isInSandbox": true,
"surface": {
"capabilities": [
{
"name": "actions.capability.SCREEN_OUTPUT"
},
{
"name": "actions.capability.ACCOUNT_LINKING"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
},
{
"name": "actions.capability.WEB_BROWSER"
}
]
},
"requestType": "SIMULATOR",
"inputs": [
{
"rawInputs": [
{
"query": "Talk to my test app",
"inputType": "KEYBOARD"
}
],
"intent": "actions.intent.MAIN"
}
],
"user": {
"userStorage": "{\"UserId\":\"ABwppHHd40lIZ1o0bRERAKlHNtNcS2qFtz7NbRQnb31AQDFuV41VPFQivXwwpQGtv_5SlsZNp0N3kxalIIXXXXXX\",\"NumVisits\":1}",
"lastSeen": "2019-05-18T19:12:38Z",
"locale": "en-US",
"userId": "ABwppHHd40lIZ1o0bRERAKlHNtNcS2qFtz7NbRQnb31AQDFuV41VPFQivXwwpQGtv_5SlsZNp0N3kxalIIXXXXXX"
},
"conversation": {
"conversationId": "ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX",
"type": "NEW"
},
"availableSurfaces": [
{
"capabilities": [
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
}
]
}
]
}
},
"session": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX"
}
Most notable from that is the session attribute set at the bottom from that. The code uses this as the parent and session values when building the
The request objects for the other two Intents are similar, and all have the same value for session. None of the response objects are notable in any way.
When the Welcome Intent is triggered, the call to listSessionEntities(), unsurprisingly, shows there aren't any yet:
info: listSessionEntities parent projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX
info: listSessionEntities [
[],
null,
null
]
The parent appears to have the correct value from the session however.
When the handler for the Welcome Intent goes and creates the Session Entity, things appear to work ok:
info: setSessionEntity request {
"parent": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX",
"sessionEntityType": {
"displayName": "code",
"entities": [
{
"value": "alpha",
"synonyms": [
"alpha"
]
},
{
"value": "bravo",
"synonyms": [
"bravo"
]
}
],
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/entityTypes/code",
"entityOverrideMode": "ENTITY_OVERRIDE_MODE_OVERRIDE"
}
}
info: setSessionEntity created [
{
"entities": [
{
"synonyms": [
"alpha"
],
"value": "alpha"
},
{
"synonyms": [
"bravo"
],
"value": "bravo"
}
],
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/entityTypes/code",
"entityOverrideMode": "ENTITY_OVERRIDE_MODE_OVERRIDE"
},
null,
null
]
The parent appears to be the same as the session, and the name appears to follow the correct format, including the additional part that has the "/entityTypes/" followed by the display name.
When I try calling it with the code "alpha", which should trigger the "entry" Intent, it instead triggers the Fallback Intent. The call to listSessionEntities() seems to show the "code" Entity with the Entity Types we expect, even tho there was no match for "alpha".
info: listSessionEntities parent projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX
info: listSessionEntities [
[
{
"entities": [
{
"synonyms": [
"alpha"
],
"value": "alpha"
},
{
"synonyms": [
"bravo"
],
"value": "bravo"
}
],
"name": "projects/session-test-XXXXX/agent/sessions/ABwppHFGTpcFtHOOo6ehQfKys9AnH14V5-RhzrNKsea6y6L5zgv4eN-j3IesfuqSsKMc7qRt1CAOhkUYA9XXXXXX/entityTypes/code",
"entityOverrideMode": "ENTITY_OVERRIDE_MODE_OVERRIDE"
}
],
null,
null
]
Again, everything looks correct. When I try it again with "zulu", it shows the same thing for the call to listSessionEntites(), but this time it matches the "entry" Intent, since "zulu" is one of the Entity Types for "code" that is defined as a Developer Entity.
This is where I am stuck. Everything looks correct. It looks like the Session Entity should be set correctly for this session. It looks like it should be using those values. But it never seems to do so.
What is going on? All help would be greatly appreciated. (Did you even read till the end of the question? If so - thank you! I know it's long, but wanted to be as complete as possible.)
This appears to be a bug - I've gotten feedback from other developers that they're seeing the same problem, sometimes on previously working code.
A bug has been opened at https://issuetracker.google.com/issues/133166381 to track the issue. Star it to indicate you have similar problems and to track progress.

How to store the values of a JSON response to an API Call in as many variables as the (unknown) number of objects using node.js

I'm creating an integration between two software, and I need to get data from software1 to post it to software2.
What I try to do is (do not hesitate if it is not the best way to do it):
First, create an API call to GET the data from software1.
Second, store the values of the JSON response in variables
Finally, use these variables to POST it back to software2
The problem is: the JSON response from API call to software 1 can contain an unknown number of objects. (Sometime it could be just, sometime it could be 2,3,...,10 or more)
So how could I create as many variables as I need to store all this data and be able to reuse it after?
I have built a program using node.js
I think I have something that makes sense if there would have only one object in the JSON response, but I need help to scale it so it could manage any number of objects.
View my code below
Here is an example of JSON response from the software1
[
{
"id": "my-awesome-program",
"currency": "USD",
"title": "My awesome program",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
}
]
But sometime it could have more than one object
[
{
"id": "my-awesome-program2",
"currency": "USD",
"title": "My awesome program 2",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
}
{
"id": "my-awesome-program1",
"currency": "USD",
"title": "My awesome program 1",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
}
]
Note that the only values that I'm interested to store are "id" and "title"
Here's the nodejs module I use:
const express = require('express');
const request = require('request-promise-native');
const NodeCache = require('node-cache');
const session = require('express-session');
const delay = require('delay');
Here's my code
//====================================================//
// Get a list of programs from software1 //
//====================================================//
const getPrograms = async () => {
console.log('Retrieving programs');
const headers = {
Authorization: `Api-Key: xxxxxx`,
'Content-Type': 'application/json'
};
const programs = {
method: 'GET',
url: 'https://api.software1.com/1.6/programs/',
headers: headers,
json: true
};
request(programs)
.then(function (parsedBody) {
console.log(parsedBody);
})
.catch(function (err) {
console.log(err);
});
// Store programs data in reusable variables
// THIS IS WHERE I NEED HELP TO CREATE AS MANY VARIABLES AS OBJECTS IN THE JSON RESPONSE
const programObj = JSON.parse(parsedBody);
const programId = programObj.id;
const programTitle = programObj.title;
};
//====================================================//
// Post the list of programs in software2 //
//====================================================//
// Creating a contact property in software2 to store programs from the list retrieved from software1
const createPrograms = async (accessToken) => {
console.log('Creating a property named programs');
const headers = {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
};
const program = {
headers: headers,
method: 'POST',
uri: 'https://api.software2.com/properties/v1/contacts/properties',
body: {
name: 'programs',
label: 'Programs',
description: 'programs in which the contact is enrolled',
groupName: 'group_of_properties',
type: 'enumeration',
fieldType: 'select',
options: [ // THIS IS WHERE I NEED HELP TO CREATE AS MANY OPTIONS AS CREATED REUSABLE VARIABLES
{
"label": programTitle,
"value": programId
},
]
},
json: true // Automatically stringifies the body to JSON
};
Thanks for your help!
I got an answer in another forum so I thought I could paste it here for the help of the community.
original answer: https://community.hubspot.com/t5/APIs-Integrations/How-to-use-the-values-of-a-JSON-response-from-an-external/m-p/269841#M24366
In order to just get the data you need from what's being returned back to you, you can use some higher order functions such as map:
great youtube video on this: https://www.youtube.com/watch?v=rRgD1yVwIvE
MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
For example, if you have this data structure:
[
{
"id": "my-awesome-program2",
"currency": "USD",
"title": "My awesome program 2",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
},
{
"id": "my-awesome-program1",
"currency": "USD",
"title": "My awesome program 1",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
}
]
you can use this code to map the objects into something more useful to you. First you can assign the data to a variable:
const myData = [
{
"id": "my-awesome-program2",
"currency": "USD",
"title": "My awesome program 2",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
},
{
"id": "my-awesome-program1",
"currency": "USD",
"title": "My awesome program 1",
"cookie_time": 45,
"default_landing_page_url": "https://my-site.com"
}
]
Then we can map over the data and assign it to a new variable or return it from a function:
const newData = myData.map(item => ({
id: item.id,
title: item.title
}))
// returns
// [
// {
// "id": "my-awesome-program2",
// "title": "My awesome program 2"
// },
// {
// "id": "my-awesome-program1",
// "title": "My awesome program 1"
// }
// ]
After this, if you need each item to then make a call to Hubspot, you can do that with whatever library you want like this:
axios.post(url, config, item).then(res => {
console.log("...Done!")
})
});
It'd be best to put this code in a separate function after you've returned all of the data you need in order to ensure that all the data is there. I'd also recommend limiting your calls per second so you don't hit the X/s rate limit of the API. A good package for that is called bottleneck on NPM.

API.ai Actions on Google - Failed to parse JSON response string with 'INVALID_ARGUMENT' error: ": Cannot find field."

This error is similar to what I asked here, but this time it's with NodeJs client.
I am trying to find directions to a location. As soon as the intent is triggered on my webhook, I am calculating the directions using GoogleMapAPI. But before it can finish and send a response, I receive the error on my Actions Console. I checked total response time and it is less than 2 seconds which is less than 5 seconds timeout by Google.Where I am wrong???
My API.ai Intent
Using express.js with Action-on-Google Node Client
'use strict';
const express = require('express');
const bodyParser = require('body-parser');
const intentHandler = require('./intent_handler')
const app = express();
app.use(bodyParser.json());
const ApiAiAssistant = require('actions-on-google').ApiAiAssistant;
// Create functions to handle requests here
....
....
const DIRECTION_INTENT = 'action_direction';
function MyAssistant(req, res) {
const assistant = new ApiAiAssistant({request: req, response: res});
assistant.handleRequest(responseHandler(assistant));
}
function responseHandler (assistant) {
// intent contains the name of the intent you defined in the Actions area of API.AI
let intent = assistant.getIntent();
switch (intent) {
case WELCOME_INTENT:
...
break;
case WELCOME_FALLBACK_PERMISSION_INTENT:
...
break;
case DIRECTION_INTENT:
console.log(">>>>>>>DIRECTION_INTENT<<<<<<<");
intentHandler.directionIntent(assistant);
break;
}
}
app.post('/', function (req, res) {
MyAssistant(req, res);
});
app.listen(8080, function () {
console.log('app listening on port 8080!')
});
Handler Code
'use strict';
const speech = require("./speech_template");
const direction = require("./directionModule");
const intent_handler = {
'welcomeIntent': function (assistant) {
.....
},
'welcomeFallbackPermissionIntent': function (assistant) {
.....
},
'directionIntent':function (assistant) {
console.log('direction intent');
direction.getDirectionWithSavedAddress(function (response) {
assistant.ask(response);
});
}
};
module.exports = intent_handler;
Direction Extraction --- ERROR comes on Action Console before this get finished
'use strict';
const striptags = require('striptags');
const speech = require("./speech_template");
let googleMapsClient = require('#google/maps').createClient({
key: global.GOOGLE_DIRECTION_KEY
});
const directionModule = {
'getDirectionWithSavedAddress': function (eventCallback) {
let myAdd = <From Saved Data>;
if (myAdd === undefined) {
console.log("error......");
}
let destination = <From Saved Data>;
this.getDirectionWithAddress(myAdd, destination, function (dir) {
....
if(SUCCESS){
eventCallback(`<speak> ${steps} </speak>`);
}else{
eventCallback(`<speak> ${speech.ERROR_DIRECTIONS} </speak>`);
}
});
},
'getDirectionWithAddress': function (add1, add2, eventCallback) {
let dir = {};
googleMapsClient.directions({
origin: add1,
destination: add2,
mode: "driving",
departure_time: "now"
}, function (err, response) {
if (!err) {
console.log(response.json.routes[0]);
....
....
....
} else {
console.log(`Error --> ${err.toString()}`);
....
}
eventCallback(dir);
});
}
};
module.exports = directionModule;
UPDATE
I am running the code locally via WebStorm and exposing webhook via port forwarding using ngrok.
Update2
BAD REQUEST 400
{
"originalRequest": {
"source": "google",
"version": "2",
"data": {
"isInSandbox": true,
"surface": {
"capabilities": [
{
"name": "actions.capability.AUDIO_OUTPUT"
}
]
},
"inputs": [
{
"rawInputs": [
{
"query": "get me there",
"inputType": "VOICE"
}
],
"arguments": [
{
"rawText": "get me there",
"textValue": "get me there",
"name": "text"
}
],
"intent": "actions.intent.TEXT"
}
],
"user": {
"locale": "en-US",
"userId": "<uID>"
},
"device": {},
"conversation": {
"conversationId": "<cID>",
"type": "ACTIVE",
"conversationToken": "[\"_actions_on_google_\",\"defaultwelcomeintent-followup\"]"
}
}
},
"id": "<ID>",
"timestamp": "2017-09-12T17:08:10.321Z",
"lang": "en",
"result": {
"source": "agent",
"resolvedQuery": "get me there",
"speech": "",
"action": "action_direction",
"actionIncomplete": false,
"parameters": {},
"contexts": [
{
"name": "_actions_on_google_",
"parameters": {},
"lifespan": 99
},
{
"name": "google_assistant_input_type_voice",
"parameters": {},
"lifespan": 0
},
{
"name": "actions_capability_audio_output",
"parameters": {},
"lifespan": 0
},
{
"name": "defaultwelcomeintent-followup",
"parameters": {},
"lifespan": 4
}
],
"metadata": {
"intentId": "<iID>",
"webhookUsed": "true",
"webhookForSlotFillingUsed": "false",
"nluResponseTime": 15,
"intentName": "DirectionIntent"
},
"fulfillment": {
"speech": "",
"messages": [
{
"type": 0,
"speech": ""
}
]
},
"score": 1
},
"status": {
"code": 200,
"errorType": "success"
},
"sessionId": "<sID>"
}
This looks like before my callback is finished, my webhook is sending empty response to Google Actions.
Why is this happening and How to resolve it?????
The problem lies in how your directionIntent() function calls, and handles the result of, your getDirectionWithSavedAddress() function. It expects getDirectionWithSavedAddress() returns a function, when it does not. Instead, getDirectionWithSavedAddress() expects to send its results to a callback.
So after it makes its call to getDirectionWithAddress(), the function ends, returning nothing. This "nothing" is sent to assistant.ask(), which returns that to Google's server. This is an invalid response, so you're getting the error.
Fixing this should be straightforward. You need to call getDirectionWithSavedAddress() with a callback function. Inside this function you should call assistant.ask() with the value sent to the callback.
So directionIntent() might look something like
'directionIntent':function (assistant) {
console.log('direction intent');
direction.getDirectionWithSavedAddress( function( msg ){
assistant.ask( msg );
} );
}
Updated
This line makes no sense:
assistant.handleRequest(responseHandler(assistant));
The assistant.handleRequest() function is supposed to be passed a Map of Intent names to functions to call to handle the event. You're doing this manually in the responseHandler() function and you're not returning a Map. Since you're not returning a Map, it fails when trying to do the handleRequest() and generates the error "Action Error: Request handler can NOT be empty".
You can fix this by just calling responseHandler(assistant) and not dealing with handleRequest() at all. Or you can create the map that handleRequest() is expecting and get rid of responseHandler() completely.

Facebook Messenger Bot Persistent Menu

I am generating my first bot working with node.js and heroku but finding some difficulties to understand the persistent menu functionalities.
Question 1) How do can I attach event as callbacks?
function persistentMenu(sender){
request({
url: 'https://graph.facebook.com/v2.6/me/thread_settings',
qs: {access_token:token},
method: 'POST',
json:{
setting_type : "call_to_actions",
thread_state : "existing_thread",
call_to_actions:[
{
type:"postback",
title:"FAQ",
payload:"DEVELOPER_DEFINED_PAYLOAD_FOR_HELP"
},
{
type:"postback",
title:"I Prodotti in offerta",
payload:"DEVELOPER_DEFINED_PAYLOAD_FOR_HELP"
},
{
type:"web_url",
title:"View Website",
url:"https://google.com/"
}
]
}
}, function(error, response, body) {
console.log(response)
if (error) {
console.log('Error sending messages: ', error)
} else if (response.body.error) {
console.log('Error: ', response.body.error)
}
})
}
Question 2) The only way I have found for empty the persistent menu and generating a new one is with a delete request via terminal ("as Facebook documented")m is there a possibily to clear inserting a refresh function on my app.js file?
curl -X DELETE -H "Content-Type: application/json" -d '{"setting_type":"call_to_actions","thread_state":"existing_thread"}' "https://graph.facebook.com/v2.6/me/thread_settingsaccess_token=PAGE_ACCESS_TOKEN"
The FB example robot is not well structured for call backs. I haven't found a good way to structure the example in Node callback or promise model. I'm sure a Node expert can reorg it.
As for the persistent menu, if you send an empty call_to_actions array the menu will disappear. The menu seems a bit 'sticky' however as it does not immediately appear/disappear when the message is sent.
I incorporated your snippet into my example robot. You can see it at
https://messenger.com/t/dynamicmemorysolutions
The source is at:
https://github.com/matthewericfisher/fb-robot
See the add/remove menu commands and functions.
EDIT: The persistent menu API has been updated. See this question for more details.
this worked for me:
function menuButton() {
var messageData = {
setting_type : "call_to_actions",
composerinputdisabled :"TRUE",
thread_state : "existing_thread",
call_to_actions:[
{
type:"postback",
title:"¿Tiempo de espera?",
payload:"ACTUALIZAR"
},
{
type:"postback",
title:"Ver Promociones",
payload:"DEVELOPER_DEFINED_PAYLOAD_FOR_START_ORDER"
}
]
}
request({
uri: 'https://graph.facebook.com/v2.6/me/thread_settings',
qs: { access_token: PAGE_ACCESS_TOKEN },
method: 'POST',
json: messageData
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
var recipientId = body.recipient_id;
var messageId = body.message_id;
console.log("Successfully sent generic message with id %s to recipient %s",
messageId, recipientId);
} else {
console.error("Unable to send message.");
console.error(response);
console.error(error);
}
});
}
And I call this function at the beggining
app.post('/webhook', function(req, res){
var data = req.body;
if (data.object == 'page') {
menuButton();
data.entry.forEach(function(entry) {
var pageID = entry.id;
var timeOfEvent = entry.time;
// Iterate over each messaging event
entry.messaging.forEach(function(event) {
if (event.message) {
receivedMessage(event);
}else if (event.postback) {
receivedPostback(event);
} else {
console.log("Webhook received unknown event: ", event);
}
});
});
res.sendStatus(200);
}
})
What I have not being able to do is to remove the option of free text input. Facebook claimed now is possible yet have found no instructions or examples on how to do it. Any clues?
If you want to disable the free text input, you shoud add the following parameter to your persistent menu request:
"composer_input_disabled": true
and not
composerinputdisabled :"TRUE"
The FB API document states that the API link to hit for applying persistent menu to the page specific bot is:
https://graph.facebook.com/v2.6/me/messenger_profile?access_token=<PAGE_ACCESS_TOKEN>
Notice the me after version number i.e v2.6 in this specific case. However, this did not worked for a lot of people
There is small change in the API link to hit:
graph.facebook.com/v2.6/Page ID/messenger_profile?access_token=PAGE ACCESS TOKEN
Notice that me is replaced with the fb Page Id.
And the sample payload can still be the same:
{
"get_started": {
"payload": "Get started"
},
"persistent_menu": [
{
"locale": "default",
"composer_input_disabled": false,
"call_to_actions": [
{
"title": "Subscribe",
"type": "postback",
"payload": "subscribe"
},
{
"title": "Stop notifications",
"type": "nested",
"call_to_actions": [
{
"title": "For 1 week",
"type": "postback",
"payload": "For_1_week"
},
{
"title": "For 1 month",
"type": "postback",
"payload": "For_1_month"
},
{
"title": "For 1 year",
"type": "postback",
"payload": "For_1_year"
}
]
},
{
"title": "More",
"type": "nested",
"call_to_actions": [
{
"title": "Fresh jobs",
"type": "postback",
"payload": "fresh jobs"
},
{
"title": "Like us",
"type": "web_url",
"url": "https://www.facebook.com/onlysoftwarejobs/"
},
{
"title": "Feedback",
"type": "web_url",
"url": "https://docs.google.com/forms/d/e/1FAIpQLScjgFRbfBLznO55kFIskcH_eFc23zRSUUxzIgv_o44uj0GMpw/viewform"
}
]
}
]
}
]
}
Notice that it is mandatory to configure get_started button before setting up the persistent_menu.

Resources