Now that you’ve deployed your well structured, exception handling, language understanding chatbot, how do you know just what people are saying to it?
Sure, you could copy and paste some logging code all over the place, but there must be a cleaner way.
In this article I’ll show you a few simple tricks to be able to save each message going to and from your botframework chatbot.
IDialog
Let’s start off by saving messages going to any dialog that implements IDialog<T>
.
In order to implement IDialog<T>
you only need to implement the StartAsync
method; however, this isn’t much use on its own, so let’s get the dialog into a conversation loop by adding in a MessageReceivedAsync
method and calling that from StartAsync
and from itself:
public class TranscribingDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
context.Wait(MessageReceivedAsync);
}
}
Right now this doesn’t do much, so let’s add in a repository for saving those messages using the ServiceResolver
, as introduced in the previous article:
public class TranscribingDialog : IDialog<object>
{
private static IMessageRepository MessageRepository
= ServiceResolver.Get<IMessageRepository>();
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
// save it!
await MessageRepository.AddMessageAsync(message);
context.Wait(MessageReceivedAsync);
}
}
Let’s assume for now that the MessageRepository implementation actually persists the message somewhere; you can put it in table storage, dynamo db, mongo db, mysql; even SQL server – entirely up to you and your own bot situation. I’ll fill in this blank in another article.
That’s the easiest part done; you can do the same for any of your dialogs that implement IDialog
.
Now on to the other dialog type, LuisDialog
:
LuisDialog
Let’s take a look at the dialog we created in a previous article:
[Serializable]
[LuisModel("modelID", "subscriptionID")]
public class WeatherLuisDialog : LuisDialog<object>
{
[LuisIntent("")]
public async Task DidntUnderstand(IDialogContext context, LuisResult result)
{
await context.PostAsync("Could you try that again?");
context.Wait(MessageReceived);
}
[LuisIntent("local.weather")]
public async Task StartWeatherDialog(IDialogContext context, LuisResult result)
{
await context.PostAsync("Most likely!");
context.Wait(MessageReceived);
}
}
LuisDialog
has a MessageReceived
method which handles the parsing of your user’s input. When you use a LuisDialog
your class will tend to call the underlying LuisDialog
’s MessageReceived
method.
See the StartWeatherDialog
method above as an example of this; the WeatherLuisDialog
itself has no MessageReceived
method, so it’s using the MessageReceived
method on the LuisDialog
instead.
I’m going to create a new base class that overrides LuisDialog
’s MessageReceived
:
[Serializable]
public class TranscribingLuisDialog<T> : LuisDialog<T>
{
private static IMessageRepository MessageRepository => ServiceResolver.Get<IMessageRepository>();
protected override async Task MessageReceived(IDialogContext context, IAwaitable<IMessageActivity> item)
{
var message = await item;
// save it!
await MessageRepository.AddMessageAsync(message);
await base.MessageReceived(context, item);
}
}
This will get the contents of the user input, persist it, then call the base class’s (LuisDialog
’s) MessageReceived
method.
To have our own dialogs use this new class is simply a case of changing the class definition to use TranscribingLuisDialog
instead of LuisDialog
:
public class WeatherLuisDialog : TranscribingLuisDialog<object>
{
...
}
That’s all! Now all dialogs that need to use LUIS will have their incoming messages saved. This is good progress! We’ve now got a solution for both IDialog
and LuisDialog
classes.
The next section is a big one – it’s going to take a while, so take a deep breath; here we go:
Prompt
When you send the user a prompt their response doesn’t fire the same MessageReceived
flow as before; instead they’re kept in a mini flow where their input must be a valid response to the prompt. Capturing this input is a little trickier, but not impossible.
If we look at the Prompt
class itself, we can see the closure that handles the response is in an abstract
class – yay! Unfortunately, all of the classes that extend it are sealed
– boo! (All in that same file)
This limits our options; since I can’t override the implementations, I will create duplicate classes that implement our own logic – and also extend the abstract class – to do the message saving.
There are a couple of approaches to this, and I’m going to go with basically overriding Prompt
with duplicate code but add in one extra line to save the message:
[Serializable]
public abstract class TranscribingPrompt<T, TOptions> : Prompt<T, TOptions>, IDialog<T>
{
private static IMessageRepository MessageRepository => ServiceResolver.Get<IMessageRepository>();
protected TranscribingPrompt(PromptOptions<TOptions> promptOptions) : base(promptOptions) { }
async Task IDialog<T>.StartAsync(IDialogContext context)
{
await context.PostAsync(this.MakePrompt(context, promptOptions.Prompt, promptOptions.Options, promptOptions.Descriptions));
context.Wait(MessageReceived);
}
private async Task MessageReceived(IDialogContext context, IAwaitable<IMessageActivity> message)
{
var content = await message;
// save it!
await MessageRepository.AddMessageAsync(content);
T result;
if (TryParse(content, out result))
{
context.Done(result);
}
else
{
--promptOptions.Attempts;
if (promptOptions.Attempts > 0)
{
await context.PostAsync(this.MakePrompt(context, promptOptions.Retry ?? promptOptions.DefaultRetry, promptOptions.Options));
context.Wait(MessageReceived);
}
else
{
//too many attempts, throw.
await context.PostAsync(this.MakePrompt(context, promptOptions.TooManyAttempts));
throw new TooManyAttemptsException(promptOptions.TooManyAttempts);
}
}
}
}
Since this implements
IDialog
I need to implementStartAsync
. Since it extendsPrompt
I can usePrompt
’s existing virtualMakePrompt
method to actually generate the prompt message.
The class above is a direct copy of Prompt
from that huge PromptDialog.cs
file.
I’ve used the same approach to get a MessageRepository
(i.e., the ServiceResolver
) and I’m calling AddMessageAsync
inside of the MessageReceived
method so that the response to the prompt is captured.
To use this I need to replace usages of Prompt
with TranscribingPrompt
; this is the time-consuming bit – since the usages are all in the sealed
classes we need to get copy-pasting…
TranscribingPrompt Example: Int64
I’m not going to paste the entire file that needs duplicating in here, since it’s HUGE, but here’s an example of what needs doing for each prompt type – the example is for PromptInt64
:
// BEFORE
[Serializable]
public sealed class PromptInt64 : Prompt<Int64, Int64>
{
public PromptInt64(string prompt, string retry, int attempts)
: base(new PromptOptions<long>(prompt, retry, attempts: attempts))
{
}
protected override bool TryParse(IMessageActivity message, out Int64 result)
{
return Int64.TryParse(message.Text, out result);
}
}
The only real difference between the various types of Prompt is their override of
Prompt
’s abstract methodTryParse
.
We can create our new TranscribingPromptInt64
by merely extending the TranscribingPrompt
abstract class in a new file which duplicates PromptInt64
:
// AFTER
[Serializable]
public class TranscribingPromptInt64 : TranscribingPrompt<Int64, Int64>
{
public TranscribingPromptInt64(string prompt, string retry, int attempts)
: base(new PromptOptions<long>(prompt, retry, attempts: attempts))
{
}
protected override bool TryParse(IMessageActivity message, out Int64 result)
{
return Int64.TryParse(message.Text, out result);
}
}
Notice the class is extending
TranscribingPrompt
instead ofPrompt
Repeat this process for – wait for it:
PromptString
PromptConfirm
PromptDouble
PromptChoice
PromptAttachment
Boom. Lots of copy-pasting.
Since Resources are internal in resx files, you’ll find a few errors in your PromptConfirm
after you copy and paste.
Just create your own Resources
using use some static properties on a new Resources
class:
public class Resources
{
public static string TooManyAttempts = "too many attempts";
public static string PromptRetry = "I didn't understand. Say something in reply.";
public static string[] MatchYes = { "yes", "y", "sure", "ok", "yep", "1" };
public static string[] MatchNo = { "no", "n", "nope", "2" };
}
You’ll also need to remove the uses of SplitList
from Options
and Patterns
in your new version of PromptConfirm
.
Prompt
dialog
You’ll also need your own PromptDialog
class, so that you can change the usual static
methods that look like this:
public static void Number(IDialogContext context, ResumeAfter<long> resume, string prompt, string retry = null, int attempts = 3)
{
var child = new PromptInt64(prompt, retry, attempts);
context.Call<long>(child, resume);
}
To methods that look like this:
public static void Number(IDialogContext context, ResumeAfter<long> resume, string prompt, string retry = null, int attempts = 3)
{
// Call our transcribing version instead
var child = new TranscribingPromptInt64(prompt, retry, attempts);
context.Call<long>(child, resume);
}
Notice I’ve changed
PromptInt64
toTranscribingPromptInt64
As such, I’ll create a new TranscribingPromptDialog
and create my own versions of the following methods, changing the type of the underlying Prompt
they use:
- Text
- Confirm
- Number (see above for one of the implementations)
- Choice
- Attachment
Using the TranscribingPrompt
Now instead of calling the static method Prompt.Number
I can call TranscribingPrompt.Number
, e.g.:
PromptDialog.Number(context, AfterConfirming, "think of a number");
becomes
TranscribingPromptDialog.Number(context, AfterConfirming, "think of a number");
Saving messages sent by the Bot
We’ve now captured the messages coming in to our bot, but what about the bot’s replies? If we include these then it’s easy enough to export an entire conversation.
To send a message to a user we use the context.PostAsync
method.
Again, a couple of approaches for altering this, but I’m just going to create a couple of extension methods on IBotToUser
:
public static class BotToUserEx
{
private static IMessageRepository MessageRepository => ServiceResolver.Get<IMessageRepository>();
public static async Task PostToUserAndTranscribeAsync(this IBotToUser botToUser, string text)
{
var message = botToUser.MakeMessage();
message.Text = text;
await MessageRepository.SaveMessage(message);
await botToUser.PostAsync(message);
}
public static async Task PostToUserAndTranscribeAsync(this IBotToUser botToUser, IMessageActivity message)
{
await MessageRepository.SaveMessage(message);
await botToUser.PostAsync(message);
}
}
To use these, simply replace your calls to context.PostAsync
with context.PostToUserAndTranscribeAsync
.
Summary
You should now have a solution for saving user messages coming in to your IDialog
dialogs, your LuisDialog
s, and your Prompt
s, and you should also be able to save the messages sent by the bot.
Hopefully you’ve found this botframework message transcribing walkthrough useful and interesting.
I’d love to know how you’re solving this problem yourself, so please let me know.
how do we save conversations from bot and user when using form flow instead of prompt Dialog? Would be great if you can write on this.
I have developed a bot using C# and deployed it using direct line api.
I am trying to find a way for the bot to receive a parameter from the website when it initially loads.
I want the same bot to work on different websites and want the bot to recognize the website using this parameter sent by the website to the bot and then act according to that website.
Is there a way to take a parameter from the website automatically when it loads ?