I'm tinkering with Actions on Google with DialogFlow + Firebase. The idea is to custom build for IoT devices not supposed by the default AoG.
In the case of Radio and TV, there are 2 intents currently:
1) Channel : It take in 3 parameters: device_name (custom entity), device_action (custom entity) and value.
Eg: please change the radio channel to 22.3
OR change the channel of the tv to 22
2) Volume : It take in 3 parameters: device_name (custom entity), device_action (custom entity) and value.
Eg: please change the radio volume to 99
OR change the vol of the tv to 22
The problem is that the agent does not seem to be able to differentiate between the two well.
Is the implementation wrong?
// Edit 9 May 2018:
index.js
// Copyright 2016, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
const util = require('util');
const functions = require('firebase-functions');
const {
dialogflow,
Suggestions,
BasicCard,
Button,
SimpleResponse,
} = require('actions-on-google');
const {values, concat, random, randomPop} = require('./util');
const responses = require('./responses');
/** Dialogflow Contexts {#link https://dialogflow.com/docs/contexts} */
const AppContexts = {
FACT: 'choose_fact-followup',
CATS: 'choose_cats-followup',
XXX: 'choose_xxx-followup',
};
/** Dialogflow Context Lifespans {#link https://dialogflow.com/docs/contexts#lifespan} */
const Lifespans = {
DEFAULT: 5,
};
const app = dialogflow({
debug: true,
init: () => ({
data: {
// Convert array of facts to map
facts: responses.categories.reduce((o, c) => {
o[c.category] = c.facts.slice();
return o;
}, {}),
cats: responses.cats.facts.slice(), // copy cat facts
},
}),
});
/**
* Greet the user and direct them to next turn
* #param {DialogflowConversation} conv DialogflowConversation instance
* #return {void}
*/
app.intent('Unrecognized Deep Link Fallback', (conv) => {
const response = util.format(responses.general.unhandled, conv.query);
const suggestions = responses.categories.map((c) => c.suggestion);
conv.ask(response, new Suggestions(suggestions));
});
// redirect to the intent handler for tell_fact
app.intent('choose_fact', 'tell_fact');
// Say a fact
app.intent('tell_fact', (conv, {category}) => {
const {facts, cats, xxx} = conv.data;
if (values(facts).every((c) => !c.length)) {
// If every fact category facts stored in conv.data is empty,
// close the conversation
return conv.close(responses.general.heardItAll);
}
const categoryResponse =
responses.categories.find((c) => c.category === category);
const fact = randomPop(facts[categoryResponse.category]);
if (!fact) {
const otherCategory =
responses.categories.find((other) => other !== categoryResponse);
const redirect = otherCategory.category;
const parameters = {
category: redirect,
};
// Add facts context to outgoing context list
conv.contexts.set(AppContexts.FACT, Lifespans.DEFAULT, parameters);
const response = [
util.format(responses.transitions.content.heardItAll, category, redirect),
];
// If cat facts not loaded or there still are cat facts left
if (cats.length) {
response.push(responses.transitions.content.alsoCats);
}
response.push(responses.general.wantWhat);
conv.ask(concat(...response));
conv.ask(new Suggestions(otherCategory.suggestion));
if (cats.length) {
conv.ask(new Suggestions(responses.cats.suggestion));
}
return;
}
const {factPrefix} = categoryResponse;
// conv.ask can be called multiple times to have the library construct
// a single response itself the response will get sent at the end of
// the function or if the function returns a promise, after the promise
// is resolved.
conv.ask(new SimpleResponse({
speech: concat(factPrefix, fact),
text: factPrefix,
}));
conv.ask(responses.general.nextFact);
conv.ask(new BasicCard({
title: fact,
image: random(responses.content.images),
buttons: new Button({
title: responses.general.linkOut,
url: responses.content.link,
}),
}));
console.log('hiwwxxxxxww thi is aaron');
conv.ask(responses.general.suggestions.confirmation);
});
// Redirect to the intent handler for tell_cat_fact
app.intent('choose_cats', 'tell_cat_fact');
// Say a cat fact
app.intent('tell_cat_fact', (conv) => {
const {cats} = conv.data;
console.log('this is cats data' + {cats});
const fact = randomPop(cats);
if (!fact) {
conv.contexts.delete(AppContexts.FACT);
conv.contexts.delete(AppContexts.CATS);
conv.ask(responses.transitions.cats.heardItAll);
return conv.ask(responses.general.suggestions.confirmation);
}
const {factPrefix, audio} = responses.cats;
// conv.ask can be called multiple times to have the library construct
// a single response itself. The response will get sent at the end of
// the function or if the function returns a promise, after the promise
// is resolved.
const sound = util.format(audio, random(responses.cats.sounds));
conv.ask(new SimpleResponse({
// <speak></speak> is needed here since factPrefix is a SSML string
// and contains audio.
speech: `<speak>${concat(factPrefix, sound, fact)}</speak>`,
text: factPrefix,
}));
conv.ask(responses.general.nextFact);
conv.ask(new BasicCard({
title: fact,
image: random(responses.cats.images),
buttons: new Button({
title: responses.general.linkOut,
url: responses.cats.link,
}),
}));
console.log('hiwwxxxxxww thi is aaron');
conv.ask(responses.general.suggestions.confirmation);
});
//say a tv channel
app.intent('volume', (conv, {device_name,device_action, value}) => {
var no_device_name = device_name;
var no_value = value;
var no_device_action = device_action;
var this_device_value = util.inspect(value, false, null);
var this_device_name = util.inspect(device_name, false, null);
var this_device_action = util.inspect(device_action, false, null);
console.log(this_device_action[0]);
if (no_device_name[0] == 'channel'){
console.log('inside tv but CHANNEL');
}
else{
console.log('VOLUME');
}
console.log('THIS IS VOL');
console.log(no_device_action[0]);
conv.ask(`Alright, ${device_name} VOLUM is now ${value}! `);
console.log('inside volume ' + value[0]);
console.log('inside volume' + device_name[0]);
console.log('inside volume' + device_action[0]);
console.log('hiwwxxxxxww thi is aaron');
});
//say a tv channel
app.intent('channel', (conv, {device_channel_name, device_channel_action, channel_value}) => {
console.log('THIS IS CHANNEL');
conv.ask(`Alright, ${device_channel_name} CHANNEL is now ${channel_value}! `);
console.log('inside CHANNEL ' + channel_value[0]);
console.log('inside CHANNEL' + device_channel_name[0]);
console.log('inside CHANNEL' + device_channel_action[0]);
console.log('hiwwxxxxxww thi is aaron');
});
app.intent('no_input', (conv) => {
const repromptCount = parseInt(conv.arguments.get('REPROMPT_COUNT'));
if (repromptCount === 0) {
conv.ask(`What was that?`);
} else if (repromptCount === 1) {
conv.ask(`Sorry I didn't catch that. Could you repeat yourself?`);
} else if (conv.arguments.get('IS_FINAL_REPROMPT')) {
conv.close(`Okay let's try this again later.`);
}
});
// The entry point to handle a http request
exports.factsAboutGoogle = functions.https.onRequest(app);
If you have a sample phrase with a part of that phrase set to represent a parameter, that part of the phrase can take on any value the parameter is defined to be. The "resolved value" column just shows what it has resolved to in your sample phrase.
So in your "channel" Intent, the phrase "tv channel to 3" can also match "radio volume to 4" since "tv" and "radio" are both of the device_name entity, and "channel" and "volume" are both of the device_action entity.
You have a few solutions:
You can turn them into one Intent that accepts all the phrases and in your webhook check the value of device_action to see what you should do.
You can keep them as separate Intents and remove the device_action parameter completely. You don't need them. Just use the synonyms in various sample phrases so the learning system gets trained about which synonyms work with which Intent.
If you are still worried about synonyms, or if it makes sense still, make them different Entity types.
For example, you may want to have "channel" and "preset" as different entities under the same Entity type. You care about the difference because it changes how you treat the number, but you're still changing the channel. "Volume" would be a different entity, since it represents something completely different.
In your comments, you asked about personalized aliases for devices. You didn't ask about more than one device type with different names (so you want to handle "tv1" and "tv2" with the same Intent, but be able to detect the difference. Both of these are handled best with option (3) above, but in slightly different ways.
If you want more than one "tv", each of those would be of the same Entity Type (for example, "device tv"), but each one would be a different entity value. It might look something like this:
"But wait!" I hear you saying "What if the user has three tvs? Or nine? Do I have to set all of these up? And what if they want to call one the 'bedroom tv' and another the 'den tv'?"
That suggests that you would keep the number of TVs and their aliases in a database of some sort (which you need to do anyway - you probably need to map their TV into some unique device ID to actually turn them on) and, when the user talks to your agent, update this with a User Entity.
User Entities have user-specific names and aliases set for just that user. You'll set them using the Dialogflow API when the user first talks to your agent. If you're using V1, you'll use the /userEntities endpoint. If you're using V2, you'll use the projects.agent.sessions.entityTypes resource.
Related
I'm coding a whitelist system for my discord bot that, on ready event (and after a 3 seconds delay), checks if every server it is in has it's ID added to the whitelist database on MongoDB. If not, the bot sends an embed and leaves the server. I managed to get it working on the guildCreate event, but on ready event it performs the message and leave actions on every single server without filtering conditions, even though those are added to the list. I cannot figure out why. Also, I'm still new to JavaScript, so it could be just a minor mistake.
//VARIABLES
const { Client, MessageEmbed } = require("discord.js")
const config = require('../../Files/Configuration/config.json');
const DB = require("../../Schemas/WhitelistDB");
//READY EVENT
module.exports = {
name: "ready",
once: false,
async execute(client) {
//[ ... ] <--- OTHER UNNECESSARY CODE IN BETWEEN
setTimeout(function() { // <--- 3 SECONDS DELAY
client.guilds.cache.forEach(async (guild) => { // <--- CHECK EVERY SERVER
await DB.find({}).then(whitelistServers => { // <--- CHECK MONGODB ID LIST
if(!whitelistServers.includes(guild.id)) {
const channel = guild.channels.cache.filter(c => c.type === 'GUILD_TEXT').random(1)[0]; // <--- SEND MESSAGE TO RANDOM TEXT CHANNEL (It is sending to every server, when it should be sending only to the not whitelisted ones)
if(channel) {
const WhitelistEmbed = new MessageEmbed()
WhitelistEmbed.setColor(config.colors.RED)
WhitelistEmbed.setDescription(`${config.symbols.ERROR} ${config.messages.SERVER_NOT_WHITELISTED}`)
channel.send({embeds: [WhitelistEmbed]});
}
client.guilds.cache.get(guild.id).leave(); // <--- LEAVE SERVER (It is leaving every server, when it should be leaving only the not whitelisted ones)
} else { return }
});
});
}, 1000 * 3);
}
}
I found the solution myself!
Instead of finding the array of whitelisted ID's for each guild, find one at a time and instead of checking the content of the array, check if the array exists. This is the updated code:
//WHITELIST
setTimeout(function() {
client.guilds.cache.forEach(async (guild) => {
await DB.findOne({ GuildID: guild.id }).then(whitelistServers => {
if(!whitelistServers) {
const channel = guild.channels.cache.filter(c => c.type === 'GUILD_TEXT').random(1)[0];
if(channel) {
const WhitelistEmbed = new MessageEmbed()
WhitelistEmbed.setColor(config.colors.RED)
WhitelistEmbed.setDescription(`${config.symbols.ERROR} ${config.messages.SERVER_NOT_WHITELISTED}`)
channel.send({embeds: [WhitelistEmbed]});
}
client.guilds.cache.get(guild.id).leave();
} else { return }
});
});
}, 1000 * 3);
I am trying to understand the cost of Datastore. It seems that it subscribes to all Mutations. So if there are 50 users, then each message will be send 50 times, even if it not required.
As each real time mutation costs money, we will be paying unnecessary 49 times for this real time message mutation.
Also , it seems to me SyncExpression doesn't have any effect on this Subscription.
I am really stuck here. It will be great of someone can clarify
Amplify generates the datastore boilerplate code for you, but you still need to call it. You won't pay for every user and every mutation.
You will only subscribe to a mutation (explicitly call the code to listen for changes) on a per-user basis for things that user is interested in. e.g. if you are viewing a TODO item, you'd subscribe the user to that item and they'll immediately see if someone else modify it on another device.
UPDATE
Long story... I was triggering back-end computation via GraphQL by making a lambda resolver. The computation took too long and the GQL call would timeout. I updated the code so the GQL call called itself asynchronously (re-trigger the lambda), and returned immediately. Then when the long-running task completed in the spun-up lambda, I updated the a record in the database.
I update the record using AppSync instead of direct GQL so it would trigger mutations, and in the react client, I listen to a mutation for the specific record that will be updated. This way, there is just 1 user listening (if they've triggered the long running action) and that user is only notified about changes to the single DB record they're interested in, and not receiving other user's updates.
I don't know if all this is applicable to your situation. The code snippets below may help you, but they're somewhat out of context.
// In amplify/backend/api/projectname/schema.graphql
type Subscription {
onCouponWithIdUpdated(id: ID!): Coupon #aws_subscribe(mutations: ["updateCoupon"])
}
// In my useSendCoupon hook...
// Subscribe to coupon updates
useEffect(() => {
if (0 === couponId) {
return
}
console.log(`subscribe to coupon updates for couponId:`, couponId)
const onCouponWithIdUpdated = /* GraphQL */ `
subscription OnCouponWithIdUpdated($id: ID!) {
onCouponWithIdUpdated(id: $id) {
id
proofLink
owner
}
}
`
const subscription = API
.graphql(graphqlOperation(onCouponWithIdUpdated, { id: couponId }))
.subscribe({
next: ({ provider, value }) => {
const coupon = value.data.onCouponWithIdUpdated
//console.log(`Proof Link:`, coupon.proofLink)
setProofLinks([coupon.proofLink])
setSendCouponState(COUPON_STATE_PREVIEW_SUCCESS)
},
error: error => console.warn(error)
})
console.log('subscribed: ', subscription)
return () => {
console.log(`unsubscribe to coupon updates`)
subscription.unsubscribe()
}
}, [couponId])
// inside a lambda...
const updateCouponWithProof = async (authorization, couponId, proofLink) => {
const initializeClient = () => new AWSAppSyncClient({
url: process.env.API_XXXX_GRAPHQLAPIENDPOINTOUTPUT,
region: process.env.REGION,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: authorization
},
disableOffline: true,
})
const executeMutation = async (mutation, operationName, variables) => {
const client = initializeClient()
try {
const response = await client.mutate({
mutation: gql(mutation),
variables,
fetchPolicy: "no-cache",
})
return response.data[operationName]
} catch (err) {
console.log("Error while trying to mutate data", err)
throw JSON.stringify(err)
}
}
const updateCoupon = /* GraphQL */ `
mutation UpdateCoupon(
$input: UpdateCouponInput!
$condition: ModelCouponConditionInput
) {
updateCoupon(input: $input, condition: $condition) {
id
proofLink
owner
}
}
`
const variables = { input: { id: couponId, proofLink } }
try {
return await executeMutation(updateCoupon, 'updateCoupon', variables)
} catch (error) {
console.log(`executeMutation error`, error)
}
}
How do we store all user input data in one document per one chat session?
I tried this code:
'use strict';
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
process.env.DEBUG = 'dialogflow:debug';
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
function getAge(agent) {
let age = agent.parameters.age;
db.collection("users").add({age: age});
}
function getLocation(agent) {
let location = agent.parameters.location;
db.collection("users").add({location: location});
}
function getCustomerExperience(agent) {
let customerExperience = agent.query;
db.collection("users").add({customerExperience: customerExperience});
}
let intentMap = new Map();
intentMap.set('age', age);
intentMap.set('location', getLocation);
intentMap.set('customer-experience', getCustomerExperience);
agent.handleRequest(intentMap);
});
but the data were stored in different document IDs:
What I'm trying to achieve is something like this:
If I'm not being clear, please let me know. I'm new to Dialogflow, Firebase, as well as the JS language. Cheers!
You're on the right track! The fundamental problem with your original code is that collection.add() will create a new document. But you want it to create a new document sometimes, and save it in a previous document other times.
This means that, during the entire Dialogflow session, you'll need some way to know what the document name is or should be. There are a few possible ways to do this.
Use a document based on the session
Dialogflow provides a session identifier that you can get as part of the agent.session property using the dialogflow-fulfillment library, or in the session property if you're parsing the JSON request body directly.
However, this string includes forward slash / characters, which should be avoided in document names. Fortunately, the format of this string is documented to be one of the two formats:
projects/Project ID/agent/sessions/Session ID
projects/Project ID/agent/environments/Environment ID/users/User ID/sessions/Session ID
In each case, the Session ID is the last portion of this path, so you can probably use code something like this to get the ID for the session, use it as your document name, and then save an attribute (for example, age) for it:
function documentRef( agent ){
const elements = agent.session.split('/');
const lastElement = elements[elements.length - 1];
return db.collection('users').doc(lastElement);
}
async function getCourier(agent) {
const ref = documentRef( agent );
const age = agent.parameters.age;
return await ref.update({age: age});
}
Note that I have also made getCourier() an async function, because the function calls that change the database (such as ref.update()) are async functions and Dialogflow requires you to either make it an async function or explicitly return a Promise. If you wish to return a Promise instead, this would be something more like this:
function getCourier(agent) {
const ref = documentRef( agent );
const age = agent.parameters.age;
return ref.update({age: age});
}
Use the document name generated by Firestore
With this method, you'll store a document name as a Context parameter. When you go to save a value, you'll check if this document name is set. If it is, you'll do an update() using this document name. If not, you'll do an add(), get the document name, and save it in the Context parameter.
It might look something like this (untested), again for the age:
async function getCourier( agent ){
const ref = db.collection('users');
const age = agent.parameters.age;
const docContext = agent.context.get('doc');
if( !docContext ){
// We don't previously have a document, so create it
const res = await ref.add({age:age});
// And set a long-lasting context with a "name" parameter
// containing the document id
agent.context.set( 'doc', 99, {
'name': ref.id
} );
} else {
// There is a context with the document name already set
// so get the name
const docName = docContext.parameters['name'];
const docRef = ref.doc(docName);
// And save the data at this location
await docRef.update({age: age});
}
}
Again, this uses an async function. If you'd rather use a Promise, it might be something more like this:
function getCourier( agent ){
const ref = db.collection('users');
const age = agent.parameters.age;
const docContext = agent.context.get('doc');
if( !docContext ){
// We don't previously have a document, so create it
return ref.add({age:age})
.then( ref => {
// And set a long-lasting context with a "name" parameter
// containing the document id
agent.context.set( 'doc', 99, {
'name': ref.id
} );
});
} else {
// There is a context with the document name already set
// so get the name
const docName = docContext.parameters['name'];
const docRef = ref.doc(docName);
// And save the data at this location
return docRef.update({age: age});
}
}
Use a document name you've generated and saved in the context
You don't need to use the session id from the first alternative. If you have some ID or name that makes sense on your own (a username or a timestamp, for example, or some combination), then you can save this in a Context parameter and use this each time as the document name. This is a combination of the first and second approaches above (but probably simpler than the second one, since you don't need to get the document name from creating the document the fist time).
first intent
second intent
As shown in the below code, the flow is not going from Number Intent to First Intent, it is been looped into the number loop. In dialog flow, with every intent corresponding context is also made. The flow is not moving as per context and is stuck in NumberIntent.
The flow should be like the google ask the user its survey id, the user says its id 612020 and google start asking its questions. The flow works fine until the type of question is rating i.e. user has to speak number. The error arises when the user is asked to answer in descriptive manner.
'use strict';
// Import the Dialogflow module from the Actions on Google client library.
const {dialogflow} = require('actions-on-google');
const functions = require('firebase-functions');
// Instantiate the Dialogflow client.
const app = dialogflow({debug: true});
const axios = require('axios').default;
global.ques = [];
global.i=0;
app.intent('Default Welcome Intent', (conv) => {
conv.add('Hello! What is your survey id?');
});
app.intent('NumberIntent', (conv,{number}) => {
return axios.get('https://www.openeyessurvey.com/api/get_open_survey_info/612020')
.then((result) => {
result.data.Item.QUESTIONS.map(questionobj => {
ques.push(questionobj.question);
})
conv.ask(ques[i]);
i+=1;
}).catch( err => {
console.log("error", JSON.stringify(err,null,2));
conv.close('This is not a valid survey ID');
});
});
app.intent('FirstIntent', (conv, {number}) => {
conv.ask(ques[i]);
i+=1;
});
app.intent('SecondIntent', (conv) => {
const des = conv.parameters.any;
if(des === 'ankit'){
conv.ask(ques[i]);
i+=1;
}
});
app.intent('ThirdIntent', (conv) => {
conv.ask(ques[i]);
i+=1;
});
app.intent('FourthIntent', (conv, {number}) => {
conv.ask(ques[i]);
i+=1;
});
app.intent('FifthIntent', (conv) => {
conv.ask(ques[i]);
i+=1;
conv.close('Goodbye!')
});
// Set the DialogflowApp object to handle the HTTPS POST request.
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);
Output
Output2
INVALID INTENT NAME ERROR
I suspect that the issue is that i never actually gets updated.
You treat ques and i as global object, but since this is running under Firebase Cloud Functions, each call may be a new instance of the function. As a new instance, these would get reinitialized.
The flip side of this is that if you didn't get a new instance, it also has the problem that this would not work correctly if more than one person was using the Action at the same time since they would all be sharing the same value of i.
The solution to both is that, instead of storing i as a global variable, store it either in the Actions on Google session storage or in a Dialogflow context parameter.
Storing it as a session parameter, you would get the value, use it, increment it, and then save it again in the session parameter. It might look something like this:
const i = conv.data.i;
conv.ask(ques[i]);
conv.data.i = i+1;
Trying to implement web-hook (with V2 dialogflow) running nodejs. Received response "MalformedResponse 'final_response' must be set.". Below is the code. To the end of POST (app.post) code block was expecting conv.close would send SimpleResponse. But that's not happening. Need help understand why this error is seen and probable direction to solve it.
Thanks
const express = require('express');
const {
dialogflow,
Image,
SimpleResponse,
} = require('actions-on-google')
const bodyParser = require('body-parser');
const request = require('request');
const https = require("https");
const app = express();
const Map = require('es6-map');
// Pretty JSON output for logs
const prettyjson = require('prettyjson');
const toSentence = require('underscore.string/toSentence');
app.use(bodyParser.json({type: 'application/json'}));
// http://expressjs.com/en/starter/static-files.html
app.use(express.static('public'));
// http://expressjs.com/en/starter/basic-routing.html
app.get("/", function (request, response) {
console.log("Received GET request..!!");
//response.sendFile(__dirname + '/views/index.html');
response.end("Response from my server..!!");
});
// Handle webhook requests
app.post('/', function(req, res, next) {
console.log("Received POST request..!!");
// Log the request headers and body, to aide in debugging. You'll be able to view the
// webhook requests coming from API.AI by clicking the Logs button the sidebar.
console.log('======Req HEADERS================================================');
logObject('Request headers: ', req.headers);
console.log('======Req BODY================================================');
logObject('Request body: ', req.body);
console.log('======Req END================================================');
// Instantiate a new API.AI assistant object.
const assistant = dialogflow({request: req, response: res});
// Declare constants for your action and parameter names
//const PRICE_ACTION = 'price'; // The action name from the API.AI intent
const PRICE_ACTION = 'revenue'; // The action name from the API.AI intent
var price = 0.0
// Create functions to handle intents here
function getPrice(assistant) {
console.log('** Handling action: ' + PRICE_ACTION);
let requestURL = 'https://blockchain.info/q/24hrprice';
request(requestURL, function(error, response) {
if(error) {
console.log("got an error: " + error);
next(error);
} else {
price = response.body;
logObject('the current bitcoin price: ' , price);
// Respond to the user with the current temperature.
//assistant.tell("The demo price is " + price);
}
});
}
getPrice(assistant);
var reponseText = 'The demo price is ' + price;
// Leave conversation with SimpleResponse
assistant.intent(PRICE_ACTION, conv => {
conv.close(new SimpleResponse({
speech: responseText,
displayText: responseText,
}));
});
}); //End of app.post
// Handle errors.
app.use(function (err, req, res, next) {
console.error(err.stack);
res.status(500).send('Oppss... could not check the price');
})
// Pretty print objects for logging.
function logObject(message, object, options) {
console.log(message);
console.log(prettyjson.render(object, options));
}
// Listen for requests.
let server = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on ' + JSON.stringify(server.address()));
});
In general, The "final_response" must be set error is because you didn't send anything back. You have a lot going on in your code, and while you're on the right track, there are a few things in the code that could be causing this error.
First - in the code, it looks like you are confused about how to send a response. You have both a call to conv.close() and the commented out assistant.tell(). The conv.close() or conv.ask() methods are the way to send a reply using this version of the library. The tell() method was used by a previous version and is no longer supported.
Next, your code looks like it is only setting up the assistant object when the routing function is called. While this can be done, it is not the usual way to do it. Typically you'll create the assistant object and setup the Intent handlers (using assistant.intent()) as part of the program initialization. This is a rough equivalent to setting up the express app and the routes for it before the request itself comes in.
The portion that sets up the Assistant and then hooks it into a route might look something like this:
const assistant = dialogflow();
app.post('/', assistant);
If you really wanted to examine the request and response objects first, you might do this as something like
const assistant = dialogflow();
app.post('/', function( req, res ){
console.log(JSON.stringify(req.body,null,1));
assistant( req, res );
});
Related to this appears to be that you're trying to execute code in the route handler and then trying to call the intent handler. Again, this might be possible, but isn't the suggested way to use the library. (And I haven't tried to debug your code to see if there are problems in how you're doing it to see if you're doing it validly.) More typical would be to call getPrice() from inside the Intent handler instead of trying to call it from inside the route handler.
But this leads to another problem. The getPrice() function calls request(), which is an asynchronous call. Async calls are one of the biggest problems that causes an empty response. If you are using an async call, you must return a Promise. The easiest way to use a Promise with request() is to use the request-promise-native package instead.
So that block of code might look something (very roughly) like this:
const rp = require('request-promise-native');
function getPrice(){
return rp.get(url)
.then( body => {
// In this case, the body is the value we want, so we'll just return it.
// But normally we have to get some part of the body returned
return body;
});
}
assistant.intent(PRICE_ACTION, conv => {
return getPrice()
.then( price => {
let msg = `The price is ${price}`;
conv.close( new SimpleResponse({
speech: msg,
displayText: msg
});
});
});
The important thing to note about both getPrice() and the intent handler are that they both return a Promise.
Finally, there are some odd aspects in your code. Lines such as res.status(500).send('Oppss... could not check the price'); probably won't do what you think they will do. It won't, for example, send a message to be spoken. Instead, the Assistant will just close the connection and say that something went wrong.
Many thanks to #Prisoner. Below is the V2 working solution based on above comments. Same has been verified on nodejs webhook (without firebase). V1 version of the code was referenced from https://glitch.com/~aog-template-1
Happy coding..!!
// init project pkgs
const express = require('express');
const rp = require('request-promise-native');
const {
dialogflow,
Image,
SimpleResponse,
} = require('actions-on-google')
const bodyParser = require('body-parser');
const request = require('request');
const app = express().use(bodyParser.json());
// Instantiate a new API.AI assistant object.
const assistant = dialogflow();
// Handle webhook requests
app.post('/', function(req, res, next) {
console.log("Received POST request..!!");
console.log('======Req HEADERS============================================');
console.log('Request headers: ', req.headers);
console.log('======Req BODY===============================================');
console.log('Request body: ', req.body);
console.log('======Req END================================================');
assistant(req, res);
});
// Declare constants for your action and parameter names
const PRICE_ACTION = 'revenue'; // The action name from the API.AI intent
var price = 0.0
// Invoke http request to obtain blockchain price
function getPrice(){
console.log('getPrice is invoked');
var url = 'https://blockchain.info/q/24hrprice';
return rp.get(url)
.then( body => {
// In this case, the body is the value we want, so we'll just return it.
// But normally we have to get some part of the body returned
console.log('The demo price is ' + body);
return body;
});
}
// Handle AoG assistant intent
assistant.intent(PRICE_ACTION, conv => {
console.log('intent is triggered');
return getPrice()
.then(price => {
let msg = 'The demo price is ' + price;
conv.close( new SimpleResponse({
speech: msg,
}));
});
});
// Listen for requests.
let server = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on ' + JSON.stringify(server.address()));
});