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.