Transcribing messages in BotFramework

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 implement StartAsync. Since it extends Prompt I can use Prompt’s existing virtual MakePrompt 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 method TryParse.

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 of Prompt

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 to TranscribingPromptInt64

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 LuisDialogs, and your Prompts, 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.

2 thoughts on “Transcribing messages in BotFramework

  1. 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.

  2. 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 ?

Leave a Reply

Your email address will not be published. Required fields are marked *