Fallback to specific intent within intent fulfillment - actions-on-google

When I checked the documentation for fulfillment error handling, I found this code:
const WELCOME_INTENT = 'Default Welcome Intent';
const NUMBER_INTENT = 'Input Number';
const NUMBER_ARGUMENT = 'num';
// you can add a fallback function instead of a function for individual intents
app.fallback((conv) => {
// intent contains the name of the intent
// you defined in the Intents area of Dialogflow
const intent = conv.intent;
switch (intent) {
case WELCOME_INTENT:
conv.ask('Welcome! Say a number.');
break;
case NUMBER_INTENT:
const num = conv.arguments.get(NUMBER_ARGUMENT);
conv.close(`You said ${num}`);
break;
}
});
I wonder if there is way to reference custom fallback intent (my.intent.fallback) (which is specific to intent, my.intent) directly (like some conv.intent("my.intent.fallback") api call) instead of conv.ask('Welcome! Say a number.');.

It sounds like you're mixing two concepts here.
The app.fallback() function is just meant to register a function that will be called if no other intent handler functions match. You shouldn't be using it to then monitor what intent was called.
You should be registering intent handler functions, including a named fallback intent, with something like
app.intent( 'fallback intent name', function(conv) )

Related

Workbox - NetworkOnly executes even if I return false

Using Workbox, I am trying to deal with graphql.
Anyone know why I am getting this error message from NetworkOnly? I am returning false in the route matcher, so I don't know why new NetworkOnly() is even executing. This only happens in offline mode (no network available).
The strategy could not generate a response for ... The underlying error is TypeError: Failed to fetch.
at NetworkOnly._handle (webpack://service-workers/./node_modules/workbox-strategies/NetworkOnly.js?:101:19)
at async NetworkOnly._getResponse (webpack://service-workers/./node_modules/workbox-strategies/Strategy.js?:155:24)
const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {
maxRetentionTime: 24 * 60
});
const isGraphqlSubmitForm = async(event) => {
const clonedRequest = event.request.clone();
const body = await clonedRequest.json();
if (body?.operationName === 'submitForm') {
return true;
}
return false; // I MADE SURE THIS IS BEING RETURNED
};
registerRoute(
isGraphqlSubmitForm,
new NetworkOnly({
plugins: [bgSyncPlugin]
}),
'POST'
);
Sorry about that—you're seeing that behavior because the truthiness of the return value from this function is used to determine whether a route matches, and an async function always returns a Promise, which is a "truthy" value.
There's a log message that warns about this if you're in Workbox's development mode.
If it's possible for you to add in a header on the requests you want to match, that would probably be the easiest way to synchronously trigger your route, since any attempt to access a request body needs to be async. Failing that, the cleanest approach would be to just match all of your GraphQL requests with a given route, and then inside of that route's handler, use whatever async logic you need to trigger different strategies for different types of traffic.

Intent not moving to next intent

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;

Getting Undefined value for SignIn.status during account linking

I am working on the Account Linking & set google Sign-IN in Linking type in Google.
I have created two intents, one will call the google Sign-In feature and the second one will read the data from google account for. eg. email id, name.
In Intent 1, I have enabled the webhook call for this intent.
In Intent 2, I have set Event to actions_intent_SIGN_IN & enabled the webhook call for this intent.
Though my these functions (Intents results) in Inline Editors are successfully executing, still I am getting Undefined value for SignIn.status, code is given below, please help.
'use strict';
const {dialogflow, SignIn} = require('actions-on-google');
const app = dialogflow({
clientId: "174911074867-tuffsr7ec28vg7brppr0ntkjutthfq8n.apps.googleusercontent.com",
});
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
function accountlinking(agent) {
var signin=new SignIn('To get your account details');
}
function testsignData(agent) {
console.log("status :"+SignIn.status);
}
let intentMap = new Map();
intentMap.set('Intent1', accountlinking);
intentMap.set('Intent2', testsignData);
agent.handleRequest(intentMap);
});
1). On my Action calling, it is asking for the Google Account linking first and after linking process only it is moving ahead. But I need to get into the action, have a little conversation and when required only then asking for the Linking. I need to call via my intent. How to do that?
2). Though my these functions (Intents results) are successfully executing, still I am getting Undefined value for SignIn.status
Your testSigninData() function is calling Signin.status, but you don't have any variable called SignIn in this function, so that is why it is undefined. Try changing your function so it accepts a conv, params and signin object that are given during a sign-in.
If you have a look at the account linking documentation you can see which parameters are provided during the accountlinking process.
Example accountlinking setup for Actions on Google
const {dialogflow, SignIn} = require('actions-on-google');
const app = dialogflow({
// REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
clientId: CLIENT_ID,
});
// Intent that starts the account linking flow.
app.intent('Start Signin', (conv) => {
conv.ask(new SignIn('To get your account details'));
});
// Create a Dialogflow intent with the `actions_intent_SIGN_IN` event.
app.intent('Get Signin', (conv, params, signin) => {
if (signin.status === 'OK') {
const payload = conv.user.profile.payload;
conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`);
} else {
conv.ask(`I won't be able to save your data, but what do you want to do next?`);
}
});
The above code uses the actions on google dialogflow handler called app. In your code you are using the WebhookClient object to handle dialogflow intents. I'm not sure if you can use the WebhookClient for actions on google accountlinking.
If it still doesn't work after you changed the testSigninDate function parameters, it might be worth trying to remove the webhookclient and see if you can use the app.intent() calls to handle your intents just like the above code example.

Loopback Operation hook receives options as empty

I'll try to explain this weird situation as simple as I can.
I've created an operation hook "before save" and make it in a mixin to add it to some models.
this mixin uses context.options to get current userId to do something.
this mixin is working perfectly if I call the operation directly (like POST /Accounts for example).
But if I call it inside a remote method, the context.options is empty, for example, if we have a method called POST /Accounts/Signup, and inside it, we call Account.create(...), the "before save" hook receives the options as empty object {}
A sandbox project has been hosted here
https://github.com/mustafamagdy/loopback-sandbox-issue
the mixin code snippet is as follows:
module.exports = function(Model, options) {
Model.observe("before save", async function(ctx) {
if (ctx.instance.id) return;
const userId = ctx.options && ctx.options.accessToken && ctx.options.accessToken.userId;
if (userId) {
//... do stuff
}
else
{
console.error("Failed to scope " + Model.name + " to user (null)");
}
});
};
After the investigation, I found this issue that talks about similar behaviour, however, the comments are very destractive. So I thoughlt to write the conclusion here for anyone who are facing the same issue.
Loopback require you to pass the options you declared from the remote method to the model method(s) if you want to receive it on operation hook, so I ended up doing so.
module.exports = function(Note) {
Note.makeNew = makeNew;
async function makeNew(options) {
await Note.create(obj, options);
}
};

Actions-on-Google: Similar intents?

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.