Smarter Conversations. Part 3 - Breadcrumbs

This post continues the smarter conversations series and today I would like to show you how to keep track of the conversation flow and help your bot remember and reason about it. Previously, in part 1, I showed how to add sentiment detection to your bot and in part 2 I explored ways to keep your dialogs more open.

In part 1 I used the following dialog to illustrate why you might want to be able to detect expressed sentiment:

User >> I’m looking for screws used for printer assembly
Bot >> Sure, I’m happy to help you. 
Bot >> Is the base material metal or plastic?
User >> metal
Bot >> [lists a few recommendations]
Bot >> [mentions screws that can form their own threads]
User >> Great! I think that's what I need
Bot >> [recommends more information and an installation video]

The highlighted phrase is not an expression of a new intent, not an answer to the bot’s prompt. It’s a positive emotional reaction to the perfectly timed recommendation about thread forming screws. We were able to capture it and present to our bot as an intent:

1
2
3
4
5
6
7
bot.dialog('affirmation', [
function (session, args, next) {
// ...
}
]).triggerAction({
matches: 'Affirmation'
});

Unlike other intents, however, the Affirmation intent can’t be fulfilled without knowing what came before it. Wouldn’t it be great if the bot had access to the conversation’s breadcrumbs? If it could reason about what was talked about before?

History Engine

While the bot framework doesn’t keep the history of triggered intents and actions beyond the active dialog stack, it’s not hard to build a simple history engine that would take care of it.

Probably the easiest way to do it is via the onSelectRoute hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
const bot = new builder.UniversalBot(connector);

bot.onSelectRoute(function (session, route) {
session.privateConversationData.history = session.privateConversationData.history || [];
session.privateConversationData.history.push(route.routeData.action);
session.save();

// Don't forget to call the default processor.
// While the "on" syntax suggests that it's an event handler,
// the onSelectRoute actually replaces the default logic with yours
this.defaultSelectRoute(...arguments);
});

The route.routeData.action is the name of the dialog that is about to be triggered. Here’s how your bot would use it:

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
const affirmations = {
'*:productLookup': (session, args, next) => {
// handle positive reaction right after product lookup
},
'*:howToOrder': (session, args, next) => {
// handle positive reaction right after ordering tips
}
}

bot.dialog('affirmation', [
function (session, args, next) {
const history = session.privateConversationData.history || [];

// The last step in the history is the one currently being executed
const affirmationFor = history[history.length - 2];
const action = affirmations[affirmationFor];

if (!action) {
session.endDialog();
} else {
action(session, args, next);
}
}
]).triggerAction({
matches: 'Affirmation',
onSelectAction: function (session, args, next) {
// keep the interrupted dialog on the stack
session.beginDialog(args.action, args);
}
});

It’s important to note that if you are using the IntentDialog, you won’t see onSelectRoute triggered for your intent.matches(). This is because the matching is handled by the dialog, not the routing system. I stopped using the IntentDialog bound to / in favor or recently added global recognizers and triggers and will soon upgrade my ecommerce bot.

Relaxed Prompt

I wanted to share one more technique that I recently discovered and started using a lot to keep my prompts more open, more relaxed.

In the product selection dialog, for example, you may find yourself giving your user a set of options to choose from and also an option to forego the selection:

...
Bot >> Would you like to look at one particular brand? 
Bot >> [lists a few brand choices as buttons] 
User >> No, thank you

The answer no, thank you is not one of the brand options and I wouldn’t render it as such either. I would like the bot to accept one of the options given and consider everything else not picked up by any other recognizer as a no, thank you answer.

All we need to do, apparently, is to make sure the bot doesn’t reprompt if it receives a wrong answer and is ready for an alternative response:

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
bot.dialog('brands', [
function (session, args, next) {
// products that match previous user's selections
const products = session.privateConversationData.products;
// distinct list of brands
const brands = [...new Set(products.map(p => p.brand))];

// will come in handy when processing the response
session.dialogData.brands = brands;
session.save();

builder.Prompts.choice(session,
'Would you like to look at one particular brand?',
brands,
{
listStyle: builder.ListStyle.button,
maxRetries: 0 // <-- No re-prompt
}));
},
function (session, args, next) {
// either one of the options provided, or something else
const reply = (args.response && arg.response.entity) ||
session.message.text;

const brands = session.dialogData.brands;

if (brands.includes(reply)) {
// continue with a list filtered down by the selected brand
} else {
// "no, thank you". continue with a full list
}
}
]);

That’s it for today. Next time I will show you how to keep a full history of a conversation and be ready to send a transcript to the customer support agent when the bot gets stuck.