Creating bots in the real world brings additional challenges with versioning and user behavior. Obviously, when you update your bot, you want to make sure you seemlessly migrate users over to the new bot, often allowing them to complete the conversation they’re currently having before showing them the new bot. Also, because Bot Framework does not time out users, you can also run into a situation where a user has left for a sizeable amount of time, and either state has changed or you simply want to offer the user the chance to start over.

Let’s break this down by first talking about how things work behind the scenes inside of Bot Framework, introduce middleware, and then put forth a couple of patterns you can implement to handle these situations.

Well, one quick note before we get started - this is a 300 level concept, so I’m making the assumption you’re already familiar with Bot Framework, and have created a few bots. If not, you can check out the Bot Framework MVA.

Bot Framework behind the scenes

A bot is, at the end of the day, simply a web service. Web services, of course, are stateless, meaning that after the message is sent from the user, and a reply returned, the connection is broken down, and all data is lost. When working with a bot, however, this poses a problem, because an operation will typically take place across multiple round trips from the user to the bot.

To handle this, Bot Framework “dehydrates” or serializes the current state of the bot for each user. When the user returns by sending a new message, the user state is “rehydrated” or reloaded, the bot determines where the user left off, and the next function is then run. You can see this happening behind the scenes when tracking your bot in the console, seeing the waterfall in action, and which step the bot is about to call. In a simple “Hello, world” bot, the user would send hello, the bot sends the prompt for the name, the user replies, and then the bot runs the next function - in this case, sending the hello message.

1
2
3
4
5
6
7
8
9
10
11
const bot = new builder.UniversalBot(
connector,
[
(session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session, results) => {
session.endConversation(`Hello, ${results.response}.`);
}
]
);

No timeout

Because we’re dealing with humans, who may take some time to respond back to the bot, there is no timeout for a session[1]. If a user leaves for a couple of hours, and then replies to the What is your name? question, the bot simply picks up where it left off, as if the user replied instantly.

The first challenge here is state may have modified - the price for an item may have changed, a reservation time may no longer be available, or the operation may simply no longer be valid. In addition, this might confuse the user in general - if you didn’t talk to a bot for a couple of days, and potentially had the chat history on your client cleared, you might not expect the bot to pick up, or even know, where you left off.

The second challenge is when updating the bot. If you modified a dialog, the bot might not be able to pick up where it left off seamlessly. You may have removed a step from a waterfall, meaning the bot will error, now knowing what to run. Or, if a step changed, it may ask the user a question that simply doesn’t make sense for the current flow.

Addressing these issues doesn’isn’t necessarily difficult, but it does take a little bit of forethought and planning. With a little bit of middleware, you can transition as needed.

Middleware

Middleware allows you to intercept messages before they’re passed into the dialog flow, where the bot processes them as normal. This is useful for logging, rerouting messages (say for bot to human handoff), or, in our case, managing how a user is directed through the dialogs offered by the bot.

To implement middleware, you call use, passing in an object that implements at least one of three methods:

  • receive, which fires when a event is received by the bot
  • send, which fires when an event is sent by the bot
  • botbuilder, which executes after receive and the message is bound to a session

The last part of botbuilder is key in our case, because it’s going to allow us to open up where the user currently sits in the flow of the dialog(s), and determine the next step.

Strategies

Let’s walk through a few strategies for managing timeouts and updating your bot. I’m not going to walk through every possibility, but rather show off a few representative solutions that will hopefully illustrate how to approach the problem, and provide enough information for you to implement what’s needed.

Adding a timeout

As mentioned above, a bot doesn’t timeout. As a result, if it’s been even a few days since the user last replied, the bot is going to attempt to pick up where it left off. This can either confuse the user, or operate on bad information, neither of which is a good thing.

Reset the conversation

The first potential solution, and the easiest to implement, is to simply reset the conversation. We’ll do this by adding a lastAccess property to userData. We’ll then query this to see how long it’s been since the user last talked to the bot, and, if necessary, reset the conversation.

We determine if the user is currently in a conversation by taking a look at the dialogStack and determining if there is at least one dialog there. Our threshold is 10 seconds, which is obviously artificially small, but I want you to be able to easily test the implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bot.use(
{
botbuilder: (session, next) => {
// grab the current time
const currentTime = Date.now();
// see how long it's been (in milliseconds) since the user last
// accessed the bot
const lastAccessGap = currentTime - session.userData.lastAccess;
// update the last access
session.userData.lastAccess = currentTime;
// see if there is a dialog and it's been greater than 10 seconds
// since the user last talked to the bot
if(session.dialogStack().length > 0 && lastAccessGap > 10000) {
// a couple of friendly messages
session.send(`Hey there! It looks like it's been a while since you were last here.`);
session.send(`Let's start over to make sure we're both on the same page.`);
// reset the conversation, sending the user to the root dialog
session.reset('/');
} else {
// allow the flow to continue as normal
next();
}
},
}
)

Ask the user what they want to do

The next possibility, and slightly more advanced, is to ask the user what they want to do. We’ll use the same timestamp technique as above, but this time we’ll route the user to a confirmation dialog. If they say yes, we’ll end the dialog, which will reprompt the user. If they say no, we’ll reset the dialog like before. Again, the threshold is artificially small for testing purposes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
bot.use(
{
botbuilder: (session, next) => {
// grab the current time
const currentTime = Date.now();
// see how long it's been (in milliseconds) since the user last
// accessed the bot
const lastAccessGap = currentTime - session.userData.lastAccess;
// update the last access
session.userData.lastAccess = currentTime;
// see if there is a dialog and it's been greater than 10 seconds
// since the user last talked to the bot
if(session.dialogStack().length > 0 && lastAccessGap > 10000) {
// begin the confirmation dialog
session.beginDialog('confirm-continue');
} else {
// allow the user to continue as normal
next();
}
},
}
)
bot.dialog('confirm-continue', [
(session, args) => {
// prompt the user
builder.Prompts.confirm(session, `It looks like you've been gone for a little while. Do you want to continue where you left off?`);
},
(session, results) => {
if(results.response) {
// redirect back to the original dialog
// the last active prompt will be re-run
session.endDialog(`Sounds good. Here's the last question I asked you:`);
} else {
// reset the conversation
session.send(`Sounds good. Let's start over.`);
session.reset('/');
}
}
]);

Some thoughts on timeouts

It’s really up to you to decide how to best manage timeouts and what works best for your bot. You might decide to time the user out after twenty minutes, or maybe a couple of hours. You might automatially reset, especially in simpler two or three turn dialogs, or to prompt the user for more complex bots. You might even go a couple steps further, detecting which dialog the user is in, and make the decision based on that.

Managing versions

Versioning a live bot is a little trickier. You want to get users onto the new bot as quickly as possible, but you probably don’t want to interrupt the current flow of a user already in the middle of a dialog. If nothing else, when you version your bot, you need to handle the fact that a user in the middle of a dialog might be returning to a dialog step that doesn’t exist, or a dialog that’s changed profoundly, meaning the message the user sent might not make sense in the flow you deployed.

Reloading an older version

If you do want to transition the user, you will need to pay a little bit of attention to how you update your bot. In theory, you could do this on a dialog by dialog basis, but the code becomes trickier as you need to figure out where the user is, and swap out the dialog. I find it much easier to simply swap out the entire bot. To help manage this, I’ll put the bot, and its associated dialogs, into a folder. When it comes time to update it, I’ll copy the entire folder structure, update the copy, and then update app.js to call the updated (new) bot.

When you need to send a user to the old bot, you simply update the library property of the session, which sets the bot that will run for the incoming message. In the code snippet below, I have both an updated and original bot in the same file for simplicity’s sake. I hope you can see from here how you would implement this with multiple folders.

In the sample below, I’m going to transition the user once their current conversation completes. You could, if you so desire, update this code to always reset the conversation, with a message of course, which would cause the user to be rerouted to the new bot. Also, the code could be streamlined a little bit, but breaking it apart like I’ve done makes it (I think) a little easier to explain with comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const connector = new builder.ChatConnector();
const bot = new builder.UniversalBot(
// this is the updated bot
connector,
[
(session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session, results) => {
session.endConversation(`Hello, ${results.response}, from the updated bot`);
}
]
);
// set a version number using duck typing
bot.version = 2;
const getOriginalBot = (connector) =>{
// this will return the original bot
const originalBot = new builder.UniversalBot(
connector,
[
(session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session, results) => {
session.endConversation(`Hello, ${results.response}, from the original bot`);
}
]
);
// set a version number by using duck typing
originalBot.version = 1;
return originalBot;
};
bot.use(
{
botbuilder: (session, next) => {
if(!session.userData.version) {
// new user. set the version
session.userData.version = bot.version;
}
// check if user is in conversation and
// if the user is on the prior version
if(session.dialogStack().length > 0 && session.userData.version < bot.version) {
// load the original bot
session.library = getOriginalBot(connector);
// pass control to the original bot
// the correc step and dialog will be executed
next();
} else {
// update the version in userData
session.userData.version = bot.version;
// pass control to the new bot
next();
}
},
}
);

If you wanted to test this, start by making the new bot version 1 and sending a message to get yourself into the dialog. Then update the version to 2, rerun the application while not restarting the conversation in Bot Emulator, and sending the second message. You should see the original bot message.

Some thoughts on versioning

Versioning is tricky, especially if you’re in a CI/CD environment where you expect to make multiple updates to the bot. Bot Framework (unfortunately?) doesn’t automatically handle transitions for you, so you will need to decide what pattern will work best for you. This is also important when developing and testing your bot in a channel such as Skype or Slack, where how to reset the state from the client isn’t overly obvious. In dev you could certainly skip the transition, but in production you’ll likely want to implement a mechanism for doing so.

Final thoughts

Once you introduce your bot to the “real world”, things change. Management of the experience becomes much more important, and dealing with timeouts and versioning allows you to ensure a good experience for your users. This is something you should thing about early on, as it’s easier to introduce this at the beginning than to an existing, complex bot.

There are numerous ways you could handle this. Hopefully I’ve provided enough of a foundation for you to create an implementation that’s right for your bot.

[1] I am referring to session in general, not to the session object in Bot Framework.

留言