Botframework Prompt dialogs until user finishes - node.js

I'm creating a chat bot for slack using Microsoft's botbuilder and LUIS.
Is there a way to keep using builder.Prompts.text() to keep asking the user if there are anymore information the user wants to put, like a for or while loop? For example I want to keep on asking the user an undefined number of times if there's a key the user wants to save and only stop when the user types done and then I will have an equal number of builder.Prompts.text() to ask the user for the values to put in each of those keys.
function (session, results, next) {
builder.Prompts.text(session, "Another key to put?");
},
function (session, results, next) {
builder.Prompts.text(session, "Value to put?");
}
It doesn't seem like I can create some sort of loop with an array that saves each key with its value, I'm not sure how to approach this.
Thanks.

What you're looking for is session.replaceDialog(); there is an example labeled 'basics-loops' on the GitHub repo for the SDK. To loop through prompts, one has to create a small dialog with the desired prompts and have the dialog restart automatically via session.replaceDialog() or session.beginDialog().
I've built a chatbot that receives key-value pairs in the scenario you specified above. The code excerpt below is the final step in my 'Loop' dialog.
function (session, results) {
var value = results.response ? results.response : null,
key = session.dialogData.key;
var pairs = session.userData.kVPairs;
var newPair = {};
newPair[key] = value;
if (key && value) {
session.userData.kVPairs.push(newPair);
console.log(pairs[pairs.length - 1]);
}
session.send('latest key-value pair added, { %s : %s }', key, value);
session.replaceDialog('Loop');
}
session.replaceDialog('Loop') is incorporated at the end of this waterfall step and takes the Id of the new dialog. The method can also take optional arguments to pass to the new dialog.
Note: While not applicable here, the difference between replaceDialog and beginDialog/endDialog is semi-obvious, when you use beginDialog, the new dialog is added to the stack. When you end that child dialog, you will be returned to the original/parent dialog. replaceDialog will end the current dialog and begin the new one.

You may use replacedialog to loop the user:
bot.dialog("/getUserKeys", [
function (session, args, next) {
session.dialogData.keys = args && args.keys ? args.keys : [];
builder.Prompts.text(session, "Another key to put?");
},
function (session, results, next) {
if (results.response === "none") {
session.endDialogWithResult({response: { keys: session.DialogData.keys }});
return;
}
session.dialogData.keys[session.dialogData.keys.length] = results.response;
session.replaceDialog("/getUserKeys", { keys: session.DialogData.keys });
}
]);

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'] // {}

React Native: Reach-Navigation and Pouch-DB - db.put not done before "refresh" callback is run

Relative newbie; forgive me if my etiquette and form here aren't great. I'm open to feedback.
I have used create-react-native-app to create an application using PouchDB (which I believe ultimately uses AsyncStorage) to store a list of "items" (basically).
Within a TabNavigator (main app) I have a StackNavigator ("List screen") for the relevant portion of the app. It looks to the DB and queries for the items and then I .map() over each returned record to generate custom ListView-like components dynamically. If there are no records, it alternately displays a prompt telling the user so. In either case, there is an "Add Item" TouchableOpacity that takes them to a screen where they an add a new item (for which they are taken to an "Add" screen).
When navigating back from the "Add" screen I'm using a pattern discussed quite a bit here on SO in which I've passed a "refresh" function as a navigation param. Once the user uses a button on the "Add" screen to "save" the changes, it then does a db.post() and adds them item, runs the "refresh" function on the "List screen" and then navigates back like so:
<TouchableOpacity
style={styles.myButton}
onPress={() => {
if (this.state.itemBrand == '') {
Alert.alert(
'Missing Information',
'Please be sure to select a Brand',
[
{text: 'OK', onPress: () =>
console.log('OK pressed on AddItemScreen')},
],
{ cancelable: false }
)
} else {
this.createItem();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsScreen');
}
}
}
>
And all of this works fine. The "refresh" function (passed as onGoBack param) works fine... for this screen. The database is called with the query, the new entry is found and the components for the item renders up like a charm.
Each of the rendered ListItem-like components on the "List screen" contains a react-native-slideout with an "Edit" option. An onPress for these will send the user to an "Item Details" screen, and the selected item's _id from PouchDB is passed as a prop to the "Item Details" screen where loadItem() runs in componentDidMount and does a db.get(id) in the database module. Additional details are shown from a list of "events" property for that _id (which are objects, in an array) which render out into another bunch of ListItem-like components.
The problem arises when either choose to "Add" an event to the list for the item... or Delete it (using another function via [another] slideout for these items. There is a similar backward navigation, called in the same form as above after either of the two functions is called from the "Add Event" screen, this being the "Add" example:
async createEvent() {
var eventData = {
eventName: this.state.eventName.trim(),
eventSponsor: this.state.eventSponsor.trim(),
eventDate: this.state.eventDate,
eventJudge: this.state.eventJudge.trim(),
eventStandings: this.state.eventStandings.trim(),
eventPointsEarned: parseInt(this.state.eventPointsEarned.trim()),
};
var key = this.key;
var rev = this.rev;
await db.createEvent(key, rev, eventData);
}
which calls my "db_ops" module function:
exports.createEvent = function (id, rev, eventData) {
console.log('You called db.createEvent()');
db.get(id)
.then(function(doc) {
var arrWork = doc.events; //assign array of events to working variable
console.log('arrWork is first assigned: ' + arrWork);
arrWork.push(eventData);
console.log('then, arrWork was pushed and became: ' + arrWork);
var arrEvents = arrWork.sort((a,b)=>{
var dateA = new Date(a.eventDate), dateB = new Date(b.eventDate);
return b.eventDate - a.eventDate;
})
doc.events = arrEvents;
return db.put(doc);
})
.then((response) => {
console.log("db.createEvent() response was:\n" +
JSON.stringify(response));
})
.catch(function(err){
console.log("Error in db.createEvent():\n" + err);
});
}
After which the "Add Event" screen's button fires the above in similar sequence to the first, just before navigating back:
this.createEvent();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsDetails');
The "refresh" function looks like so (also called in componentDidMount):
loadItem() {
console.log('Someone called loadItem() with this.itemID of ' + this.itemID);
var id = this.itemID;
let totalWon = 0;
db.loadItem(id)
.then((item) => {
console.log('[LOAD ITEM] got back data of:\n' + JSON.stringify(item));
this.setState({objItem: item, events: item.events});
if (this.state.events.length != 0) { this.setState({itemLoaded: true});
this.state.events.map(function(event) {
totalWon += parseInt(event.eventPointsEarned);
console.log('totalWon is ' + totalWon + ' with ' +
event.eventPointsEarned + ' having been added.');
});
};
this.setState({totalWon: totalWon});
})
.catch((err) => {
console.log('db.loadItem() error: ' + err);
this.setState({itemLoaded: false});
});
}
I'm at a loss for why the List Screen refreshes when I add an item... but not when I'm doing other async db operations with PouchDB in what I think is similar fashion to modify the object containing the "event" information and then heading back to the Item Details screen.
Am I screwing up with Promise chain someplace? Neglecting behavior of the StackNavigator when navigating deeper?
The only other difference being that I'm manipulating the array in the db function in the non-working case, whereas the others I'm merely creating/posting or deleting/removing the record, etc. before going back to update state on the prior screen.
Edit to add, as per comments, going back to "List screen" and the opening "Item Details" does pull the database data and correctly shows that the update was made.
Further checking I've done also revealed that the console.log in createEvent() to print the response to the db call isn't logging until after some of the other dynamic rendering methods are getting called on the "Item Details" screen. So it seems as though the prior screen is doing the get() that loadItem() calls before the Promise chain in createEvent() is resolving. Whether the larger issue is due to state management is still unclear -- though it would make sense in some respects -- to me as this could be happening regardless of whether I've called my onGoBack() function.
Edit/bump: I’ve tried to put async/await to use in various places in both the db_ops module on the db.get() and the component-side loadItem() which calls it. There’s something in the timing of these that just doesn’t jive and I am just totally stuck here. Aside from trying out redux (which I think is overkill in this particular case), any ideas?
There is nothing to do with PDB or navigation, it's about how you manage outer changes in your depending (already mounted in Navigator since they are in history - it's important to understand - so componentDidMount isn't enough) components. If you don't use global state redux-alike management (as I do) the only way to let know depending component that it should update is passing corresponding props and checking if they were changed.
Like so:
//root.js
refreshEvents = ()=> { //pass it to DeleteView via screenProps
this.setState({time2refreshEvents: +new Date()}) //pass time2refreshEvents to EventList via screenProps
}
//DeleteView.js
//delete button...
onPress={db.deleteThing(thingID).then(()=> this.props.screenProps.refreshEvents())}
//EventList.js
...
constructor(props) {
super(props);
this.state = {
events: [],
noEvents: false,
ready: false,
time2refreshEvents: this.props.screenProps.time2refreshEvents,
}
}
static getDerivedStateFromProps(nextProps, currentState) {
if (nextProps.screenProps.time2refreshEvents !== currentState.time2refreshEvents ) {
return {time2refreshEvents : nextProps.screenProps.time2refreshEvents }
} else {
return null
}
}
componentDidMount() {
this._getEvents()
}
componentDidUpdate(prevProps, prevState) {
if (this.state.time2refreshEvents !== prevState.time2refreshEvents) {
this._getEvents()
}
}
_getEvents = ()=> {
//do stuff querying db and updating your list with actual data
}

Adding multiple dialogs to Microsoft botframework dialog stack

According to Microsoft's Botframework Documentation here, by using triggerAction with onSelectAction, you can add dialogs to the top of the stack if a user's utterance includes a matched phrase.
However, if the user's utterance includes TWO matched phrases, how can you add multiple dialogs to the stack?
For example, if a user said...
I want a burger and fries
I would like to add the burgers dialog and the fries dialog to the stack, so we can ask questions about both of them.
I've tried something like this:
bot.dialog('burgers', require('./burgers'))
.triggerAction({
matches: [/burger/i],
onSelectAction: (session, args, next) => {
session.beginDialog(args.action, args);
}
});
bot.dialog('fries', require('./fries'))
.triggerAction({
matches: [/fries/i],
onSelectAction: (session, args, next) => {
session.beginDialog(args.action, args);
}
});
Here's an example of the burgers dialog (the fries dialog is the same):
var builder = require('botbuilder');
var Store = require('./store');
module.exports = [
// Destination
function (session) {
session.send('Burger dialog test');
builder.Prompts.text(session, 'I am just testing the burger dialog');
},
function (session, results, next) {
session.send('Now we should go to the next dialog in the stack', results.response);
session.endDialog();
},
];
However, only one of the dialogs gets invoked... and then it's game over!
Any help is appreciated!
As you've found, only one dialog will be triggered at one time, so as a workaround to trigger multiple dialogs, we can trigger one dialog first and analyses the user input to call different child dialog.
For example:
bot.dialog('addOrder', (session, args)=>{
var text = session.message.text;
var found = text.match(/burger/i);
if(found!=null){
session.beginDialog('burger');
}
var found = text.match(/fries/i);
if(found!=null){
session.beginDialog('fries');
}
}).triggerAction({
matches: [/burger/i, /fries/i]
});
bot.dialog('burger',(session)=>{
session.send("burgers");
//logic of 'burger' dialog
session.endDialog();
});
bot.dialog('fries', (session)=>{
session.send("fries!");
//logic of 'fries' dialog
session.endDialog();
});
As you can see here, we can use a regular expression array to trigger the addOrder dialog first and then call other dialogs inside this addOrder dialog.
Or you may train a LUIS and use it in your bot like this:
const LuisModelUrl = 'YOUR-BOT-ENDPOINT';
var recognizer = new builder.LuisRecognizer(LuisModelUrl);
var intents = new builder.IntentDialog({recognizers:[recognizer]})
.matches('MyOrder',(session, args)=>{
var entities = args.entities;
//handle entities
});
bot.dialog('/',intents);
I create a intent named MyOrder and two entities named MyOrder.Burgers and MyOrder.Fries like this:

Data validation before next dialog waterfall step in Bot Framework

Have simple waterfall dialog:
SendMessageDialog = [
function (session) {
builder.Prompts.time(session, "Enter dates?");
},
function (session, results) {
session.conversationData.start = builder.EntityRecognizer.resolveTime([results.response]).toISOString();
if(typeof results.response.resolution.end != "undefined")
session.conversationData.end = results.response.resolution.end.toISOString();
}
];
Bot successfully recognizes time in different formats, and if format is invalid makes default prompt to user proposing to re-enter data like:
I didn't understand. Please choose an option from the list.
In Prompts option I can only change this default retryPrompt message. What if I need additional validation like:
User enters a date, but the date isn't valid because of business
logic (in the past, unavailable)
User enters a location, so need to check against list of available locations (perhaps from an api call)
Check number range after Prompts.number()
etc
Is there is an easy way to add additional validation to retry same waterfall step and ask user to re-enter data? How to implement this? Is there a workable code for BotBuilder 3.9?
There are some examples exist to make some validations with LUIS API calls, however they work only on next waterfall step. Goal not to go to the next step until correct data entered - is it possible? Thanks!
Right after the question was asked, had found how-to Create custom prompts to validate input:
Result code:
[
function (session) {
// Call start/end time prompt dialog
session.beginDialog("DatePromptDialog");
},
...
]
DatePromptDialog = [
function (session, args) {
var options = { retryPrompt: "I didn’t recognize dates you entered. Please try again using format: start - end dates" };
if (args && args.reprompt && args.endTimeMissed) {
builder.Prompts.time(session, "Please specify both start - end times:", options);
} else if (args && args.reprompt && args.dateInPast){
builder.Prompts.time(session, "That date seems to be in the past! Please enter a valid date.", options);
} else {
builder.Prompts.time(session, "Enter dates?", options);
}
},
function (session, results) {
var args = {};
delete session.conversationData.start; // Clear previous values
delete session.conversationData.end;
// Get start time
session.conversationData.start = builder.EntityRecognizer.resolveTime([results.response]).toISOString();
// Get duration end time if available
if(typeof results.response.resolution.end != "undefined")
session.conversationData.end = results.response.resolution.end.toISOString();
else {
args.endTimeMissed = true;
args.reprompt = true;
}
// Convert dates from string
var currDate = new Date(); // Current date
var startDate = new Date(session.conversationData.start);
var endDate = new Date(session.conversationData.end);
if(startDate < currDate || endDate < currDate) {
args.dateInPast = true;
args.reprompt = true;
}
if (args.reprompt) {
// Repeat the dialog
session.replaceDialog('DatePromptDialog', args);
} else {
// Success
session.endDialog();
}
}
];

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