Tallan's Blog

Tallan’s Experts Share Their Knowledge on Technology, Trends and Solutions to Business Challenges

Global Message Handling and Catching the Bot Framework Way

So you’ve set up your new chat bot using the Microsoft Bot Framework, and you need to catch messages before they’re read by your dialogs or FormFlows to apply some global or preemptive logic. Luckily, the Bot Framework team thought ahead and built in a concept for handling message interception, called Scorables.

What Are Scorables?

A Scorable is a class that you can register with Autofac (the dependency injection solution used by Bot Framework), that will silently sit in between your externally-facing code accepting incoming user messages, and your internal dialog code. Each one will get the chance to handle the incoming message, and even compete for priority (which is where the “score” originates).

When To Use Scorables

Since they have full access to Autofac and the dialog stack, they can allow you to do anything from:

  • applying logic across your entire set of dialogs without duplicating code (and while still being able to detect where in the conversation you are!),
  • redirecting the user by replacing the entire dialog stack, or by placing a new situational dialog on top of the current stack,
  • to allowing the user to access global settings or actions (such as the Settings menu in the Contoso Flowers sample bot, or the controls for the Alarm Bot sample).

If you need to do any of the above, or to just in general provide a way to converse outside the current dialog, Scorables might be your answer.

How To Use Scorables

The set of all Scorables that have been registered to the Autofac container are run in a loop as part of the initial, internal processing of messages, and prior to the receipt of the message by any active dialogs.

The code snippets below were all written in C#, and validated against Bot Framework 3.8.0.

First, you will want to make sure that your user messages are being delivered to your bot in a way that allows the built-in Bot Framework response logic to be applied, which will recruit your Scorables automatically.

If you don’t have an existing solution working with Bot Framework, the following steps will assume you’ve set up the .NET Quickstart Bot.

1. Create a root/default dialog for demonstration.
[Serializable]
public class SimpleDialog : IDialog<object>
{
    public async Task StartAsync(IDialogContext context)
    {
        context.Wait(MessageReceivedAsync);
    }

    public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> argument)
    {
        await context.PostAsync("Hey, I got your message!");
        context.Wait(MessageReceivedAsync);
    }
}

We’ll call this dialog as our root in order to make sure that we have a stable reference. Once it’s been created, it should persist for the lifetime of the conversation.

2. Locate your default or root dialog, and add the Conversation.SendAsync call.

Alter the controller you’re using for bot messages to use the following pattern:

    
if (activity.Type == ActivityTypes.Message)
{
    using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
    {
        var dialog = scope.Resolve<SimpleDialog>();
        await Conversation.SendAsync(activity, () => dialog);
    }
}

This should replace the body of the MessagesController created as part of the Quickstart bot / Bot Application template, depending on how you’ve set up your project. This resolves a new copy of our SimpleDialog, and adds the incoming activity to the existing dialog stack, or to the new copy of our SimpleDialog if this is a new conversation.

3. Create a Scorable to intercept a “Hello” message.
[Serializable]
public class HelloScorable : ScorableBase<IActivity, bool, double>
{
    private readonly IBotToUser _botToUser;

    public HelloScorable(IBotToUser botToUser) // Make sure the IBotToUser object has been added so we can respond to the user asynchronously!
    {
        SetField.NotNull(out _botToUser, nameof(_botToUser), botToUser);
    }

    protected override async Task<bool> PrepareAsync(IActivity item, CancellationToken token)
    {
        var message = item as IMessageActivity;

        if (message != null && !string.IsNullOrWhiteSpace(message.Text))
        {
            return message.Text == "Hello";
        }

        return false;
    }

    protected override bool HasScore(IActivity item, bool state)
    {
        return state;
    }

    protected override double GetScore(IActivity item, bool state)
    {
        return state ? 1.0 : 0;
    }

    protected override async Task PostAsync(IActivity item, bool state, CancellationToken token)
    {
        await _botToUser.PostAsync("Hello to you too!");
    }

    protected override Task DoneAsync(IActivity item, bool state, CancellationToken token)
    {
        return Task.CompletedTask;
    }
}

This will scan the incoming messages for “Hello”, and provide a TRUE state if found. This will indicate that the Scorable has a score (“HasScore”) of 1.0 (“GetScore”), and if it’s the highest value Scorable out of the set of Scorables with scores, it will receive the PostAsync action instead of the active dialog.

Note that the second type parameter is set to a bool, and all of the “state” items are of type bool. This is because this sample uses the ScorableBase abstract class, rather than the IScorable interface.

Why bother? The ScorableBase helps by defining exactly what you use to define your state:

public abstract class ScorableBase<Item, State, Score> : IScorable<Item, Score>

Since the State is now a typed generic parameter, we can ensure that we’re passing around a consistent item without worrying about typecasts like in the base IScorable interface, which treats all of the comparable fields as a vague object-type.

4. Autofac Configuration

Run the following commands in the Package Manager Console to make sure that all of the features you might need are available.

Update-Package Microsoft.Bot.Builder
Install-Package Autofac.Mvc5

Then add the following code to Global.asax.cs in order to register your custom objects in Autofac. If you’ve got Modules set up, or an existing AutofacConfig.cs, you can convert the code to fit those as well.

 Conversation.UpdateContainer(
     builder =>
     {
         builder.RegisterType<SimpleDialog>()
             .AsSelf()
             .As<IDialog<object>>();
         builder.RegisterType<HelloScorable>()
             .AsSelf()
             .As<IScorable<IActivity, double>>();
         builder.RegisterControllers(typeof(WebApiApplication).Assembly);
    }
);

DependencyResolver.SetResolver(new AutofacDependencyResolver(Conversation.Container));
5. Try It Out

Now, assuming everything is configured, you should be able to run your bot. Try talking to it with the Bot Framework Emulator and see how it responds to “Hello” and other messages to test your message interception!

 

bot_framework_hello_howdy

As you can see, the Scorables provide a powerful global intermediate layer between incoming messages and your dialogs and forms, and really shows how powerful some of the Bot Framework features are.

For more information, please feel free to comment below, or to visit the ever-expanding Bot Framework documentation on this and other topics!

_________________________________________________________________________________________

To learn more about Chatbots and how Tallan can help you automate business processes while maximizing customer engagement with Chatbot Technology, CLICK HERE.

2 Comments. Leave new

I am writing global message handler to come out of the PromptDialog using,

IDialogTask task;
task.Reset();

but I don’t want to return to the RootDialog and stay in the same Dialog that is currently processing the user’s message by breaking out of the current conversation.

So I would like to know how I can clear all the Dialog stack and add the dialog that was processing the user’s input to the stack and also do the same thing but without clearing the dialog stack.

Matthew Gajdosik
February 6, 2018 9:12 am

Depending on your use case, you could consider using IDialogStack.InterruptAsync(…) to start a new dialog with a given input. This would allow you to return to your original dialog stack once your new dialog finishes, while also passing some information to that dialog so that it can respond immediately to what you’ve intercepted.

If you need to return to the original dialog, but are getting errors due to your child dialog returning something unexpected, I’ve had some luck with the following code:

IDialogTask dialogTask;
IDialognewDialog = ... ;

// Check to see if the dialog stack is empty or not
if (dialogTask.Frames.Count > 2)
{
// If not empty, ignore the results of the new dialog when returning
newDialog = newDialog.Void();
}

dialogTask.Call(newDialog, null);

And if you need to start a dialog after calling the dialogTask.Reset() method, you should be able to use dialogTask.Call(…) with the dialog you’d like to start, followed by an await dialogTask.PollAsync(default(CancellationToken)); to recheck for “work” in the dialog stack.

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

\\\