In this example we are going to ask the user for name, age and a flight date. The challenge is to validate the input, store it in memory and send prompts to the user until we have all the infos. This example, which I took from the official docs (including slight changes made by myself), uses concepts from articles I posted earlier, such as Managing State.
For the age and date input, we use the Microsoft/Recognizers-Text libraries to perform the initial parsing. So we have to install the dependency first.
npm install @microsoft/recognizers-text-suite
Step 1: Creating state objects
We need to store the user’s answers in state. So let’s create conversationState and userState and pass them to our MyBot class.
// index.ts // ... const memoryStorage = new MemoryStorage(); const conversationState = new ConversationState(memoryStorage); const userState = new UserState(memoryStorage); const myBot = new MyBot(conversationState, userState);
In previous articles our bot’s “entry point” of processing the requests has been onTurn:
// index.ts
// ...
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
await myBot.onTurn(context);
});
});
Now we will use run instead:
// index.ts
// ...
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
await myBot.run(context);
});
});
…because this time our MyBot class extends ActivityHandler which gives us the opportunity to use on<Event> methods such as
// bot.ts
// ...
export class MyBot extends ActivityHandler {
constructor(conversationState, userState) {
// ...
this.onMessage(async (turnContext, next) => {
// ...
});
this.onDialog(async (context, next) => {
// ...
});
}
}
Step 2: Creating state properties
- userData: which will store name, age and date of our user
- conversationData: which will store the last question that we asked the user
export class MyBot extends ActivityHandler {
private userData;
private conversationData;
private conversationState;
private userState;
constructor(conversationState, userState) {
super();
this.conversationData = conversationState.createProperty(CONVERSATION_FLOW_PROPERTY);
this.userData = userState.createProperty(USER_PROFILE_PROPERTY);
// The state management objects for the conversation and user.
this.conversationState = conversationState;
this.userState = userState;
}
}
Step 3: Save state changes
Next, we need to store state changes. We use onDialog for that:
constructor(conversationState, userState) {
// ...
this.onDialog(async (context, next) => {
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
Step 4: Ask the user for input
We want to react whenever the user sends a message. We add onMessage to the constructor:
constructor(conversationState, userState) {
// ...
this.onMessage(async (turnContext, next) => {
const flow = await this.conversationData.get(turnContext, { lastQuestionAsked: question.none });
const profile = await this.userData.get(turnContext, {});
await MyBot.fillOutUserProfile(flow, profile, turnContext);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
Our method fillOutUserProfile will
- validate the user input
- check ConversationState for the last asked question
- send back the next question to the user
// src/ bot.ts
static async fillOutUserProfile(flow, profile, turnContext) {
const input = turnContext.activity.text;
let result;
switch (flow.lastQuestionAsked) {
// If we're just starting off, we haven't asked the user for any information yet.
// Ask the user for their name and update the conversation flag.
case question.none:
await turnContext.sendActivity("Let's get started. What is your name?");
flow.lastQuestionAsked = question.name;
break;
// If we last asked for their name, record their response, confirm that we got it.
// Ask them for their age and update the conversation flag.
case question.name:
result = this.validateName(input);
if (result.success) {
profile.name = result.name;
await turnContext.sendActivity(`I have your name as ${ profile.name }.`);
await turnContext.sendActivity('How old are you?');
flow.lastQuestionAsked = question.age;
break;
} else {
// If we couldn't interpret their input, ask them for it again.
// Don't update the conversation flag, so that we repeat this step.
await turnContext.sendActivity(result.message || "I'm sorry, I didn't understand that.");
break;
}
// If we last asked for their age, record their response, confirm that we got it.
// Ask them for their date preference and update the conversation flag.
case question.age:
result = this.validateAge(input);
if (result.success) {
profile.age = result.age;
await turnContext.sendActivity(`I have your age as ${ profile.age }.`);
await turnContext.sendActivity('When is your flight?');
flow.lastQuestionAsked = question.date;
break;
} else {
// If we couldn't interpret their input, ask them for it again.
// Don't update the conversation flag, so that we repeat this step.
await turnContext.sendActivity(result.message || "I'm sorry, I didn't understand that.");
break;
}
// If we last asked for a date, record their response, confirm that we got it,
// let them know the process is complete, and update the conversation flag.
case question.date:
result = this.validateDate(input);
if (result.success) {
profile.date = result.date;
await turnContext.sendActivity(`Your cab ride to the airport is scheduled for ${ profile.date }.`);
await turnContext.sendActivity(`Thanks for completing the booking ${ profile.name }.`);
await turnContext.sendActivity('Type anything to run the bot again.');
flow.lastQuestionAsked = question.none;
profile = {};
break;
} else {
// If we couldn't interpret their input, ask them for it again.
// Don't update the conversation flag, so that we repeat this step.
await turnContext.sendActivity(result.message || "I'm sorry, I didn't understand that.");
break;
}
}
}
And here methods for validation:
// Validates name input. Returns whether validation succeeded and either the parsed and normalized
// value or a message the bot can use to ask the user again.
static validateName(input) {
const name = input && input.trim();
return name !== undefined
? { success: true, name: name }
: { success: false, message: 'Please enter a name that contains at least one character.' };
};
// Validates age input. Returns whether validation succeeded and either the parsed and normalized
// value or a message the bot can use to ask the user again.
static validateAge(input) {
// Try to recognize the input as a number. This works for responses such as "twelve" as well as "12".
try {
// Attempt to convert the Recognizer result to an integer. This works for "a dozen", "twelve", "12", and so on.
// The recognizer returns a list of potential recognition results, if any.
const results = Recognizers.recognizeNumber(input, Recognizers.Culture.English);
let output;
results.forEach(result => {
// result.resolution is a dictionary, where the "value" entry contains the processed string.
const value = result.resolution['value'];
if (value) {
const age = parseInt(value);
if (!isNaN(age) && age >= 18 && age <= 120) {
output = { success: true, age: age };
return;
}
}
});
return output || { success: false, message: 'Please enter an age between 18 and 120.' };
} catch (error) {
return {
success: false,
message: "I'm sorry, I could not interpret that as an age. Please enter an age between 18 and 120."
};
}
}
// Validates date input. Returns whether validation succeeded and either the parsed and normalized
// value or a message the bot can use to ask the user again.
static validateDate(input) {
// Try to recognize the input as a date-time. This works for responses such as "11/14/2018", "today at 9pm", "tomorrow", "Sunday at 5pm", and so on.
// The recognizer returns a list of potential recognition results, if any.
try {
const results = Recognizers.recognizeDateTime(input, Recognizers.Culture.English);
const now = new Date();
const earliest = now.getTime() + (60 * 60 * 1000);
let output;
results.forEach(result => {
// result.resolution is a dictionary, where the "values" entry contains the processed input.
result.resolution['values'].forEach(resolution => {
// The processed input contains a "value" entry if it is a date-time value, or "start" and
// "end" entries if it is a date-time range.
const datevalue = resolution['value'] || resolution['start'];
// If only time is given, assume it's for today.
const datetime = resolution['type'] === 'time'
? new Date(`${ now.toLocaleDateString() } ${ datevalue }`)
: new Date(datevalue);
if (datetime && earliest < datetime.getTime()) {
output = { success: true, date: datetime.toLocaleDateString() };
return;
}
});
});
return output || { success: false, message: "I'm sorry, please enter a date at least an hour out." };
} catch (error) {
return {
success: false,
message: "I'm sorry, I could not interpret that as an appropriate date. Please enter a date at least an hour out."
};
}
}
The result will look like this:

No comments yet.