dialogflow with node.js how do you swtich intents - node.js

I have an intent that has a required parameter and a fulfilment. When you answer it takes you to my node.js application which currently looks like this:
app.intent(QUESTIONS_INTENT, (conv, params) => {
let data = conv.data;
let categoryId = data.categoryId;
let formulas = data.formulas;
let options = {
'method': 'PUT',
'url': apiUrl + 'questions/filter',
'body': {
categoryId: categoryId,
organisationId: organisation,
formulas: formulas
},
'json': true
};
return request(options).then(response => {
// We need to change how we get these
var questionText = params.questions;
var questions = response.filter(item => item.text === questionText);
data.questions = questions;
conv.ask(questions[0].text);
conv.contexts.set('colour');
}, error => {
conv.ask(JSON.stringify(error));
});
})
Currently it gets the questionText and finds the question that matches what you said. What I want it to do is to swap to a new intent. I have tried by conv.contexts.set('colour') but that doesn't appear to work.
I have an intent setup with input context as "Colour" so I would expect that when my fulfilment completes, it should swap to that intent, but it doesn't.
Can someone help me with this?

You need to make sure that you have a colour intent setup in DialogFlow that will handle the input from the user and respond to them. The question should be asked after you set the context.
return request(options).then(response => {
// We need to change how we get these
var questionText = params.questions;
var questions = response.filter(item => item.text === questionText);
data.questions = questions;
// Sets the context to colour.
conv.contexts.set('colour', 1);
// Now that the context is in control you'll be able to ask a question.
conv.ask(questions[0].text);
You'll also want to provide a lifespan after the context arg. This will make the context active for that many prompts, in the example above I set it for one so your question context will only be active for that specific response.
conv.contexts.set('colour', 1);

Related

Move data in Waterfall-Dialog. Bot Framework SDK

I'm using Bot Framework SDK with nodejs to implement a disamibuation flow.
I want that if two intents predicted by Luis are close to each other, ask the user from which of them are the one they want. I have done the validator but, I have a problem with the flow.
It is a waterfall Dialog with 3 steps:
FirstStep: Calls Orchestrator and Luis to get intents and entities. It pass the data with return await step.next({...})
Disamiguation Step: Checks if it is necessary to disambiguate, and, in that case, prompts the options. If not, it pass the data like the first step.
Answer step: If it has a disambiguation flag in the data it receives in step.result, it prompts the answer acordingly with the user response. Elsewhere, it uses the data in step.result that comes from the first step.
The problem is that, when it prompts user to say the intent, I lost the data of the FirstStep since I cannot use step.next({...})
¿How can I maintain both the data from the first step and the user answer in the prompt?
Here are the basic code:
async firstStep(step) {
logger.info(`FinalAnswer Dialog: firstStep`);
let model_dispatch = await this.bot.get_intent_dispatch(step.context);
let result = await this.bot.dispatchToTopIntentAsync(step.context, model_dispatch.model)
// model_dispatch = orchestrator_model
// result = {topIntent: String, entities: Array, disamibiguation: Array}
return await step.next({ model_dispatch: model_dispatch, result: result})
}
async disambiguationStep(step) {
logger.info(`FinalAnswer Dialog: disambiguationStep`);
if (step.result.result.disambiguation) {
logger.info("We need to disambiguate")
let disambiguation_options = step.result.result.disambiguation
const message_text = "What do you need";
const data = [
{
"title": "TEXT",
"value": disambiguation_option[0]
},
{
"title": "TEXT",
"value": disambiguation_option[1]
},
]
let buttons = data.map(function (d) {
return {
type: ActionTypes.PostBack,
title: d.title,
value: d.value
}
});
const msg = MessageFactory.suggestedActions(buttons, message_text);
return await step.prompt(TEXT_PROMPT, { prompt: msg });
return step.next(step.result) //not working
}
else {
logger.info("We dont desambiguate")
return step.next(step.result)
}
}
async answerStep(step) {
logger.info(`FinalAnswer Dialog: answerStep`);
let model_dispatch = step.result.model_dispatch
let result = step.result.result
//Show answer
return await step.endDialog();
}
You can use the step dictionary to store your values. The complex dialogs sample on GitHub is excellent for demonstrating this. https://github.com/microsoft/BotBuilder-Samples/blob/main/samples/javascript_nodejs/43.complex-dialog/dialogs/topLevelDialog.js
You can save data in the context with whatever name you want:
step.values['nameProperty'] = {}
This will be accessible within the entire execution context of the waterfall dialog:
const data = step.values['nameProperty'] // {}

Multiple urls in a get request with axios.all, how do I match response.data to its appropriate url object?

I am building a tool to use for work. Basically I upload a csv to extract details which will act as parameters in an axios get request.
I am using multiple urls in axios.all, and my problem is I cannot match up the reponse data to each object of that specific url. The details are below with code snippets. I hope I've made it clear enough below, but this has to do with mass requesting many urls at once, and receiving response data. The problem lies in matching up that response data to its correct url from which it was called.
Here we go...to start, I am mapping an array of vehicle data I am uploading from an external csv file. 'resultsArray' is my array and it holds the year, make, model, trim, price, a url to locate the original posting, and location of the vehicle.
let vehicle_specs = resultsArray.map(function(d, index) {
let values = {
year: d['Title_name'].split(' ')[0], // Iterate with bracket notation
make: d['Title_name'].split(' ')[1],
model: d['Title_name'].split(' ')[2],
trim: d['Title_name'].split(' ')[3],
price: d['Title_Price'],
cl_url: d['Title_Price_url'],
cl_location: d['Title_Location'],
}
return values;
});
I use the new keyword to create an object of the vehicle.
let Vehicle = function(year, make, model, trim, price, url, cl_url, cl_location) {
this.year = year;
this.make = make;
this.model = model;
this.trim = trim;
this.price = price;
this.url = url;
this.cl_url = cl_url;
this.cl_location = cl_location;
}
I then build the object with a new instance of Vehicle and return each vehicle as I need it to be.
let vehicle_data = vehicle_specs.map(function(s) {
let url = `http://api.marketcheck.com/v2/stats/car?api_key={}&ymm=${s.year}|${s.make}|${s.model}`;
let new_vehicle = new Vehicle(`${s.year}`, `${s.make}`, `${s.model}`, `${s.trim}`, `${s.price}`, `${url}`, `${s.cl_url}`, `${s.cl_location}`);
return new_vehicle;
});
I extract the URL's in the following code snippet and use axios.all to request data from each one.
let urls = vehicle_data.map(function(m) {
return m.url;
})
let options = {
'method': 'GET',
'headers': {
'Host': 'marketcheck-prod.apigee.net'
}
};
axios.all(urls.map(url => {
request(url, options, function (error, response, body) {
if(error) {
console.log(error);
} else {
console.log(response);
}
});
}))
My Problem:
I am using a 3rd Party API (Marcketcheck) - Holds data on vehicles.
The response data comes back (See below as an example. This is data for just 1 url)
{"price_stats":{"geometric_mean":3413,"min":899,"median":3595,"population_standard_deviation":1323,"variance":1750285,"ax":7995,"mean":3655,"trimmed_mean":3572,"standard_deviation":1323,"iqr":1800},"miles_stats":{"geometric_mean":97901,"min":2,"median":125000,"population_standard_deviation":51713,"variance":2147483647,"max":230456,"mean":125182,"trimmed_mean":125879,"standard_deviation":51713,"iqr":74734},"dom_stats":{"geometric_mean":100,"min":1,"median":100,"population_standard_deviation":399,"variance":159152,"max":2513,"mean":247,"trimmed_mean":162,"standard_deviation":399,"iqr":217},"count":101}
I cannot figure out how to match up each response data to the vehicle object of that specific url.
For example, if I request 3 urls from the vehicle object. Let's name them:
Url-1
Url-2
Url-3
I get my response data back as objects:
OBJ-1
OBJ-2
OBJ-3
I have no way as far as I know with my level of knowledge, how to assign each object back to it's specific URL and THEN, match up that OBJ data with it's specific vehicle.
I haven been beating my head against a wall for about 4 days, I cannot figure this out.
Any suggestions are welcome and I really appreciate anybody looking at this post to help out.
Check this out
let requests = urls.map((url) => {
return axios.get(url, {
headers: {
'Host': 'marketcheck-prod.apigee.net'
}
});
});
Promise.all(requests).then((responces) => {
console.log(responces);
}).catch((err) => {
console.log(err)
});

How to provide custom names for page view events in Azure App Insights?

By default App Insights use page title as event name. Having dynamic page names, like "Order 32424", creates insane amount of event types.
Documentation on the matter says to use trackEvent method, but there are no examples.
appInsights.trackEvent("Edit button clicked", { "Source URL": "http://www.contoso.com/index" })
What is the best approach? It would be perfect to have some sort of map/filter which would allow to modify event name for some pages to the shared name, like "Order 23424" => "Order", at the same time to leave most pages as they are.
You should be able to leverage telemetry initializer approach to replace certain pattern in the event name with the more "common" version of that name.
Here is the example from Application Insights JS SDK GitHub on how to modify pageView's data before it's sent out. With the slight modification you may use it to change event names based on their appearance:
window.appInsights = appInsights;
...
// Add telemetry initializer
appInsights.queue.push(function () {
appInsights.context.addTelemetryInitializer(function (envelope) {
var telemetryItem = envelope.data.baseData;
// To check the telemetry item’s type:
if (envelope.name === Microsoft.ApplicationInsights.Telemetry.PageView.envelopeType) {
// this statement removes url from all page view documents
telemetryItem.url = "URL CENSORED";
}
// To set custom properties:
telemetryItem.properties = telemetryItem.properties || {};
telemetryItem.properties["globalProperty"] = "boo";
// To set custom metrics:
telemetryItem.measurements = telemetryItem.measurements || {};
telemetryItem.measurements["globalMetric"] = 100;
});
});
// end
...
appInsights.trackPageView();
appInsights.trackEvent(...);
With help of Dmitry Matveev I've came with the following final code:
var appInsights = window.appInsights;
if (appInsights && appInsights.queue) {
function adjustPageName(item) {
var name = item.name.replace("AppName", "");
if (name.indexOf("Order") !== -1)
return "Order";
if (name.indexOf("Product") !== -1)
return "Shop";
// And so on...
return name;
}
// Add telemetry initializer
appInsights.queue.push(function () {
appInsights.context.addTelemetryInitializer(function (envelope) {
var telemetryItem = envelope.data.baseData;
// To check the telemetry item’s type:
if (envelope.name === Microsoft.ApplicationInsights.Telemetry.PageView.envelopeType || envelope.name === Microsoft.ApplicationInsights.Telemetry.PageViewPerformance.envelopeType) {
// Do not track admin pages
if (telemetryItem.name.indexOf("Admin") !== -1)
return false;
telemetryItem.name = adjustPageName(telemetryItem);
}
});
});
}
Why this code is important? Because App Insights use page titles by default as Name for PageView, so you would have hundreds and thousands of different events, like "Order 123132" which would make further analysis (funnel, flows, events) meaningless.
Key highlights:
var name = item.name.replace("AppName", ""); If you put your App/Product name in title, you probably want to remove it from you event name, because it would just repeat itself everywhere.
appInsights && appInsights.queue you should check for appInsights.queue because for some reason it can be not defined and it would cause an error.
if (telemetryItem.name.indexOf("Admin") !== -1) return false; returning false will cause event to be not recorded at all. There certain events/pages you most likely do not want to track, like admin part of website.
There are two types of events which use page title as event name: PageView
and PageViewPerformance. It makes sense to modify both of them.
Here's one work-around, if you're using templates to render your /orders/12345 pages:
appInsights.trackPageView({name: TEMPLATE_NAME });
Another option, perhaps better suited for a SPA with react-router:
const Tracker = () => {
let {pathname} = useLocation();
pathname = pathname.replace(/([/]orders[/])([^/]+), "$1*"); // handle /orders/NN/whatever
pathname = pathname.replace(/([/]foo[/]bar[/])([^/]+)(.*)/, "$1*"); // handle /foo/bar/NN/whatever
useEffect(() => {
appInsights.trackPageView({uri: pathname});
}, [pathname]);
return null;
}

How to pass params from one intent to another dialogflow

how does one pass the params from a intent in the fulfillment editor to another followup intent.
Here is my code
// original intent
// the params defined here are the ones I want to pass to the app.intent below.
app.intent('Book Appointment', (conv, params) => {
// the params that i want to access in the next intent
const date = params['date'];
const time = params['time'];
const firstName = params['given-name'];
const email = params['email'];
// what i dont get is what to put in here is it
// how am i formatting this?
const parameters = {'date':params.date,}
// is the 'welcome-context' replaced with this one from the JSON? like so?
conv.contexts.set('projects/myproject-120c2/agent/sessions/c2c1c9e0-2ccf-0cf0-d7ce-e52561d44de3/contexts/bookappointment-followup"', 5, parameters);
// or is it
conv.contexts.set('bookappointment-followup', 5, parameters); // ??
// like it is in Dialogflow
})
// follow-up intent
app.intent('Book Appointment - yes', (conv, params) => {
// this is where I want to access the ones above
// then im accessing them like this?
const conv.contexts.get('bookappointment-followup').parameters['whatever it is above'];
})
The variables in the email return undefined. I reckon it's something to do with the contexts but I dont know if its the same code like #book.appointment.follow.date etc.
Help is appreciated, cheers
You may check my answer here
Use the output context to save parameters
{
"fulfillmentText":"This is a text response",
"fulfillmentMessages":[ ],
"source":"example.com",
"payload":{
"google":{ },
"facebook":{ },
"slack":{ }
},
"outputContexts":[
{
"name":"<Context Name>",
"lifespanCount":5,
"parameters":{
"<param name>":"<param value>"
}
}
],
"followupEventInput":{ }
}
If you are using NodeJS client
You can save context with parameters like
let param1 = [];
let param2 = {};
let ctx = {'name': '<context name>', 'lifespan': 5, 'parameters': {'param1':param1, 'param2': param2}};
agent.setContext(ctx);
and get it like
let params = agent.getContext("<context name>").parameters;
let param1 = params.param1;
let param2 = params.param2;
If using conv, you may also try like this:
app.intent('<INTENT>', conv => {
conv.ask('<RESPONSE>');
const parameters = {'param1':param1, 'param2': param2}};
conv.contexts.set('welcome-context', 5, parameters);
});
and access it like here :
const conv.contexts.get(<context>).parameters[<param>];
If via context doesn't work, you may try with data like
conv.data.someProperty = 'someValue'
as stated here
Thanks this worked.
const firstName = conv.contexts.get('bookappointment-followup').parameters['given-name'];
:)

how to stop bot to not move forward unless entity is resolves

var intent = args.intent;
var number = builder.EntityRecognizer.findEntity(intent.entities, 'builtin.numer');
when i use findentity it move forward if the answer is correct or not how can i use entity resolve on that which are not builtin entites
var location1 = builder.EntityRecognizer.findEntity(intent.entities, 'Location');
var time = builder.EntityRecognizer.resolveTime(intent.entities);
when i use resolve time it ask againand again unless entity is resolve;
var alarm = session.dialogData.alarm = {
number: number ? number.entity : null,
timestamp: time ? time.getTime() : null,
location1: location1? location1.entity :null
};
/* if (!number & !location1 time)
{} */
// Prompt for number
if (!alarm.number) {
builder.Prompts.text(session, 'how many people you are');
} else {
next();
}
},
function (session, results, next) {
var alarm = session.dialogData.alarm;
if (results.response) {
alarm.number = results.response;
}
I believe I've already answered this question on StackOverflow: "Botframework Prompt dialogs until user finishes".
You'll need to create a mini-dialog, that will have at least two waterfall steps. Your first step will take any args and check/set them as the potential value your chatbot is waiting for. It'll prompt the user to verify that these are the correct values. If no args were passed in, or the data was not valid, the user will be prompted to supply the value the chatbot is waiting for.
The second step will take the user's response to the first step and either set the value into a session data object (like session.userData or session.conversationData) or restart the dialog using session.replaceDialog() or session.beginDialog().
In your main dialog you'll modify the step where you employ your EntityRecognizers to include an if-statement that begins your mini-dialog. To trigger the if-statement, you could use the same design as shown in this GitHub example or in your code. This code might look like below:
var location1 = builder.EntityRecognizer.findEntity(intent.entities, 'Location');
session.userData.location1 = location1 ? location1.entity : null;
if(!session.userData.location1) {
session.beginDialog('<get-location-dialog>');
}

Resources