Prompt input validation - node.js

The context is MS Bot Framework on Node. I need to custom validate an input from a Prompt.text or Prompt.Number. I have been taking a look at examples and docs. I have implemented a quick example with DialogAction.validatePrompt(), which works ok but has the problem that I cannot (or at least I don't know how) customize the message in case the validation fails.
The validation could fail for many reasons and it would be great to be able to choose a response message based on the failure reason.
Also I have seen the basics-custom-prompt example in: (https://github.com/Microsoft/BotBuilder/tree/master/Node/examples/basics-customPrompt) and it uses and IntentDialog to get the desired behavior. It also mentions it is a replacement for the basics-validatedPrompt example.
My questions are:
Which one is recommend to use, validatedPrompt or the IntentDialog
approach?
Is the validatedPrompt() going to be deprecated?
Does the validatedPrompt() provide a mechanism for custom message?

Microsoft Bot Framework Version 4 Prompt Validation Example link is specified in below :
https://github.com/Microsoft/BotBuilder-Samples/tree/master/samples/javascript_nodejs/10.prompt-validations
For Proper Text Prompt Validation is you create separate file which perform validation task
Code: namePrompt/index.js
const { TextPrompt } = require('botbuilder-dialogs');
module.exports.NamePrompt = class NamePrompt extends TextPrompt {
constructor(dialogId) {
super(dialogId, async (prompt) => {
if (!prompt.recognized.succeeded) {
await prompt.context.sendActivity('Please tell me your name.');
return false;
} else {
const value = prompt.recognized.value;
if (value.length < 1) {
await prompt.context.sendActivity('Your name has to include at least one character.');
return false;
} else if (value.length > 50) {
await prompt.context.sendActivity(`Sorry, but I can only handle names of up to 50 characters. Yours was ${ value.length }.`);
return false;
} else {
return true;
}
}
});
}
};
Now inside your current dialog you can import above file:
const { NamePrompt } = require('../../prompts/namePrompt');
const GET_NAME_PROMPT = 'namePrompt';
this.addDialog(new NamePrompt(GET_NAME_PROMPT));
inside your dialog step you can prompt such like this:
async promptForName(step) {
return await step.prompt(GET_NAME_PROMPT, `What is your name, human?`);
}

Related

Returning a value from a mix of async/sync function from a provider (different script) to express server

Apologies for asking this question - I know there are tons of information about async functions out there but I seem to have tried everything and cannot find a solution..
First of all let me outline the architecture of my program. There are two scripts: a main server script (node.js, express), which processes GET requests and provider script, which deals with the blockchain in the background to return some values. The server script is responsible for invoking a method that returns a value from the provider. The provider does all the work.
The snippet of the provider script:
getInfo(index, account, key) {
//Waiting on an asynchronous method, which does some work in the blockchain in the background; everything functions as it should be
try {
await this.getBlockchain
(
index
, account
, key
).then(result => {
// Here instead I invoke a SYNCHRONOUS method, which simply formats the response in a correct way
const reply = this.sendReply(result)
console.log(reply) //Logs the correct reply in the format in which the server is expecting it
return reply;
});
}
catch (error) {
return { error: 003, result: false };
}
}
The snippet of the server script:
server.get("/getAccount", async (req, res) => {
let index = req.query.index;
let account = req.query.account;
let key = req.query.key;
// Here I also check for the validity of the query values, irrelevant to this issue
// The provider class is imported as provider, hence, the provider.method (this has been tested many times before)
try {
await provider.getInfo(index, account, key).then(reply => {
const { error: infoError, result: infoValue } = reply
if (infoError == false) {
res.send(`${infoValue}`);
} else {
res.send(`${infoError}`);
};
});
}
catch (error) {
res.send("008");
}
}
);
I honestly have no idea how to approach this; I tried self-contained async function on the server side as well as different syntax but the reply is always undefined even though the reply from a synchronous call in the provider is correct.
Could someone help me to understand what I'm doing wrong? This is my first time working with async with numerous scripts and functions and I'm finding it very confusing.
Thank you so much!
With your current structure, you need to return the result of the await so that the top level of your function is returning something from the async function.
async getInfo(index, account, key) {
try {
let retVal = await this.getBlockchain(index, account, key).then(result => {
return this.sendReply(result);
});
return retVal;
} catch (error) {
return { error: 003, result: false };
}
}
But, really, it's a better coding style to not mix await and .then() and to just go with one style like this:
async getInfo(index, account, key) {
try {
let result = await this.getBlockchain(index, account, key);
return this.sendReply(result);
} catch (error) {
return { error: 003, result: false };
}
}
Note, this function never rejects because it's catching its own rejections and turning it into a resolved value. So, the caller cannot use .catch() to see errors. The caller must always check for the error property in the resolved object. This is not usually how you program with promises. It can be made to work, but often does not meet the expectations of the caller (as errors are usually communicated back via rejected promises).
This has to be a dup. but... Don't mix await and .then.
You simply try/catch around await.
try {
const reply = await provider.getInfo(index, account, key);
const { error: infoError, result: infoValue } = reply
if (infoError == false) {
res.send(`${infoValue}`);
} else {
res.send(`${infoError}`);
};
} catch (error) {
res.send(500);
}

Microsoft Teams Bot-Framework Skill Dialog: endDialog() does not work

I am implementing a dialogRootBot that will call a skillDialogBot.
My dialogRootBot is able to invoke the skillDialogBot. The conversation can progress until the point where the skillDialogBot is supposed to return the results to the dialogRootBot.
The skillDialogBot has the following setup
this.addDialog(new TextPrompt(TEXT_PROMPT))
.addDialog(new ConfirmPrompt(CONFIRM_PROMPT))
.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.processStep.bind(this)
]));
The processStep is laid out like this
async processStep(stepContext) {
const details = stepContext.options;
details.result = {
status: 'success'
};
return await stepContext.endDialog(stepContext.options);
}
I was expecting the dialogRootBot to get the result from the skillDialogBot after processStep has called endDialog, but that never happens. Instead, the user is stuck with the skillDialogBot until the user manually types in "abort", which is the command the dialogRootBot is monitoring to cancel all dialogs in its onContinueDialog() implementation
Here is how the onContinueDialog() looks like
async onContinueDialog(innerDc) {
const activeSkill = await this.activeSkillProperty.get(innerDc.context, () => null);
const activity = innerDc.context.activity;
if (activeSkill != null && activity.type === ActivityTypes.Message && activity.text) {
if (activity.text.toLocaleLowerCase() === 'abort') {
// Cancel all dialogs when the user says abort.
// The SkillDialog automatically sends an EndOfConversation message to the skill to let the
// skill know that it needs to end its current dialogs, too.
await innerDc.cancelAllDialogs();
return await innerDc.replaceDialog(this.initialDialogId, { text: 'Request canceled!' });
}
}
return await super.onContinueDialog(innerDc);
}
I modeled this after the botbuilder-samples\samples\javascript_nodejs\81.skills-skilldialog sample. If I were to change the skillDialogBot and have it do a ConfirmPrompt() before the finalStep()'s endDialog(), then the conversation ends correctly with the skillDialogBot() posting the dialog's results to the rootDialogBot.
For the sake of clarity, this is how the bookingDialog in the skills-skilldialog sample looks like
/**
* Confirm the information the user has provided.
*/
async confirmStep(stepContext) {
const bookingDetails = stepContext.options;
// Capture the results of the previous step.
bookingDetails.travelDate = stepContext.result;
const messageText = `Please confirm, I have you traveling to: ${ bookingDetails.destination } from: ${ bookingDetails.origin } on: ${ bookingDetails.travelDate }. Is this correct?`;
const msg = MessageFactory.text(messageText, messageText, InputHints.ExpectingInput);
// Offer a YES/NO prompt.
return await stepContext.prompt(CONFIRM_PROMPT, { prompt: msg });
}
/**
* Complete the interaction and end the dialog.
*/
async finalStep(stepContext) {
if (stepContext.result === true) {
const bookingDetails = stepContext.options;
return await stepContext.endDialog(bookingDetails);
}
return await stepContext.endDialog();
}
Is it not possible to endDialog() without a prompt? Why is my skillDialogBot unable to endDialog and pass the results to the rootDialogBot?
Thank You
For the sake of reference, when using stepContext.beginDialog(), it appears that we cannot reliably endDialog() if the waterfall dialog does not have a prompt step added. If there is a use case where we want to use a skillDialogBot and call a specific dialog in the skillDialogBot via stepContext.beginDialog(), and the called dialog is only doing processing (eg. call REST APIs) without the need to prompt users for further information, at the end of the processing, we can opt to end the conversation instead.
Hence, assuming we have finalStep() bound to the waterfall dialog as the last step, just use the following in finalStep()
return await stepContext.context.sendActivity({
type: ActivityTypes.EndOfConversation,
code: EndOfConversationCodes.CompletedSuccessfully,
value: stepContext.options
});
At least for my use case, I was able to terminate the dialog without needing a user prompt and got the results back to the rootDialog.

Compare API response against itself

I am trying to:
Poll a public API every 5 seconds
Store the resulting JSON in a variable
Store the next query to this same API in a second variable
Compare the first variable to the second
Print the second variable if it is different from the first
Else: Print the phrase: 'The objects are the same' if they haven't changed
Unfortunately, the comparison part appears to fail. I am realizing that this implementation is probably lacking the appropriate variable scoping but I can't put my finger on it. Any advice would be highly appreciated.
data: {
chatters: {
viewers: {
},
},
},
};
//prints out pretty JSON
function prettyJSON(obj) {
console.log(JSON.stringify(obj, null, 2));
}
// Gets Users from Twitch API endpoint via axios request
const getUsers = async () => {
try {
return await axios.get("http://tmi.twitch.tv/group/user/sixteenbitninja/chatters");
} catch (error) {
console.error(error);
}
};
//Intended to display
const displayViewers = async (previousResponse) => {
const usersInChannel = await getUsers();
if (usersInChannel.data.chatters.viewers === previousResponse){
console.log("The objects are the same");
} else {
if (usersInChannel.data.chatters) {
prettyJSON(usersInChannel.data.chatters.viewers);
const previousResponse = usersInChannel.data.chatters.viewers;
console.log(previousResponse);
intervalFunction(previousResponse);
}
}
};
// polls display function every 5 seconds
const interval = setInterval(function () {
// Calls Display Function
displayViewers()
}, 5000);```
The issue is that you are using equality operator === on objects. two objects are equal if they have the same reference. While you want to know if they are identical. Check this:
console.log({} === {})
For your usecase you might want to store stringified version of the previousResponse and compare it with stringified version of the new object (usersInChannel.data.chatters.viewers) like:
console.log(JSON.stringify({}) === JSON.stringify({}))
Note: There can be issues with this approach too, if the order of property changes in the response. In which case, you'd have to check individual properties within the response objects.
May be you can use npm packages like following
https://www.npmjs.com/package/#radarlabs/api-diff

Waterfall prompt using activity.text instead of actually prompting user

I have a dialog where a prompt in the first step is getting skipped. I determined that what is happening is that somehow step.context.activity.text is automatically being interpreted as the prompt response. For example, if I trigger the dialog with "Expedite my order", step.context.activity.text in the first step is "Expedite my order", the prompt is skipped, and step.result in the next step is "Expedite my order".
I tried creating a "buffer step" as step 1 that just did return await step.next(), but the same activity.text was then captured as the prompt response in step 2!
Interestingly, I created a mock dialog test with mocha and it did NOT exhibit these issues. I have other dialogs within the bot that also have a text prompt on step one and they are not exhibiting these issues either in mocha tests OR running in emulator (or deployed on Azure). I did note there is one difference within step.context: the dialogs that are working have step.context.responded = false and the one which is skipping the prompt has step.context.responded = true`. But I have no idea why this is set in one case but not others. This seems to be an important part of the puzzle.
I did "solve" this by setting step.context.activity.text = '' at the beginning of the step, but that seems like a bad practice. Any idea on why this is happening within this prompt? Below is my dialog code through that first step.
const { TextPrompt, ChoicePrompt, ChoiceFactory, ComponentDialog, WaterfallDialog } = require('botbuilder-dialogs');
const { oemLocatorHelper } = require('../helpers/oemLocatorHelper');
const WATERFALL_DIALOG = 'waterfallDialog2';
const CHOICE_PROMPT = 'choicePrompt';
const TEXT_PROMPT = 'textPrompt';
const ESCALATION_OPTIONS = ['Chat','Email','Call','No Thanks'];
class escalationDialog extends ComponentDialog {
constructor(dialogId, userDialogStateAccessor, userState) {
super(dialogId);
this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
this.addDialog(new TextPrompt(TEXT_PROMPT));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.promptAccount.bind(this),
this.promptChannel.bind(this),
this.promptSummary.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
// State accessors
this.userDialogStateAccessor = userDialogStateAccessor;
this.userState = userState;
} // End constructor
async promptAccount(step) {
if (step.context.responded) {
step.context.activity.text = '';
}
const userData = await this.userDialogStateAccessor.get(step.context, {});
if (userData.accountNumber) {
return userData.accountNumber;
} else {
return await step.prompt(TEXT_PROMPT, `To help me get you to the right agent, can you please provide me your account number?`);
}
}
EDIT: Should be step.context.activity.text, not step.activity.text. Updated in several spots. Also, the change now clears this value only if step.context.responded is true.
I believe the problem is that your bot class is calling continueDialog on the same turn that it begins escalationDialog. I have commented on your private GitHub issue.

Botframework with NodeJS while prompt system doesn't read what user input the value

Azure BotFramework
SDK4
NodeJS
In dialog state I am using 'return await step.prompt' inside async function. But once user enters the value it is not considering the user input value as a input for prompt instead it is going to luisrecognizer for the match.
I have written the similar code for different dialog where it works fine.
Kindly requesting you provide some valuable input.
Azure BotFramework
SDK4
NodeJS
this.addDialog(new WaterfallDialog('OrderStatusDialog', [
async function(step) {
return await step.context.sendActivity(reply);
},
async function(step) {
var cardValue = step.context.activity.value;
if (cardValue) {
if (cardValue.action == "Order_Number") {
return await step.prompt('textPrompt', 'enter order number');
} else if (cardValue.action == "Serial_Number") {
return await step.prompt('textPrompt', 'enter serial number');
} else {
}
}
return await step.next();
// return await step.endDialog();
},
async function (step) {
var cardValue = step.context.activity;
console.log("****** cardValue");
console.log(cardValue);
console.log("****** step After");
console.log(step);
return await step.endDialog();
}
]));
at prompt step it should read the value what user is entering. Also stack is empty when i console the step ..
Unfortunately, you have a few issues with your code. But, hopefully this will help iron them out. I had no issues running the below code.
One, double check which version of Botbuilder you have installed. At least one call ('this.addDialog') I am unfamiliar with, doesn't appear to be a part of the current SDK, and didn't work for me when testing.
Two, configure your bot like below. Technically, you should be able pass the various steps in as you have them in your question. However, something about yours was not work and I couldn't figure out what. The below setup DOES work, however, and conforms to already accepted practices.
Three, your first step calls 'step.context.sendActivity(reply)'. In the next step you are trying to read the returned value of that call. That isn't going to work as sendActivity simply sends a statement from the bot to the user ("Welcome to my bot!", for example). You need to perform a prompt to capture the user's input response (see below).
It looks like you are trying to read a value from a card. Since you didn't supply that bit of code, I faked the value by supplying the 'Order_Number' and 'Serial_Number' via a text prompt from the user.
You should also take advantage of the bot state options. Rather than use the variable 'cardValue', consider using UserState or DialogState to store user values important to the conversation.
Lastly, in this simple example, order and serial numbers will overwrite each other if you pass thru multiple times.
const START_DIALOG = 'starting_dialog';
const DIALOG_STATE_PROPERTY = 'dialogState';
const USER_PROFILE_PROPERTY = 'user';
const ACTION_PROMPT = 'action_prompt';
const ORDER_PROMPT= 'order_prompt';
const SERIAL_PROMPT= 'serial_prompt';
...
class ExampleBot {
constructor(conversationState, userState) {
this.conversationState = conversationState;
this.userState = userState;
this.dialogState = this.conversationState.createProperty(DIALOG_STATE_PROPERTY);
this.userData = this.userState.createProperty(USER_PROFILE_PROPERTY);
this.dialogs = new DialogSet(this.dialogState);
this.dialogs
.add(new TextPrompt(ACTION_PROMPT))
.add(new TextPrompt(ORDER_PROMPT))
.add(new TextPrompt(SERIAL_PROMPT));
this.dialogs.add(new WaterfallDialog(START_DIALOG, [
this.promptForAction.bind(this),
this.promptForNumber.bind(this),
this.consoleLogResult.bind(this)
]));
}
async promptForAction(step) {
return await step.prompt(ACTION_PROMPT, `Action?`,
{
retryPrompt: 'Try again. Action??'
}
);
}
async promptForNumber(step) {
const user = await this.userData.get(step.context, {});
user.action = step.result;
if (user) {
if (user.action == 'Order_Number') {
return await step.prompt(ORDER_PROMPT, 'enter order number');
} else if (user.action == 'Serial_Number') {
return await step.prompt(SERIAL_PROMPT, 'enter serial number');
} else {
}
}
return await step.next();
}
async consoleLogResult(step) {
const user = await this.userData.get(step.context, {});
user.orderNumber = step.result;
console.log('****** cardValue');
console.log(user);
console.log('****** step After');
console.log(step);
return await step.endDialog();
}
async onTurn(turnContext) {
... //Other code
// Save changes to the user state.
await this.userState.saveChanges(turnContext);
// End this turn by saving changes to the conversation state.
await this.conversationState.saveChanges(turnContext);
}
}
Hope of help!
somehow it worked for me after saving conversation state where ever i am replacing the dialog.
await this.conversationState.saveChanges(context);

Resources