Users may want to say “help” in middle of a dialog. As a developer, you can implement global message handler to handle these “keywords”. Read the article here for more detail.
Let’s implement one of the most common global handler, “cancel”.
1. Add Scorables folder in O365Bot project, and add CancelScorable.cs. In this class, you specify “cancal” as keyword and take action whenever user sends the keyword.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Builder.Internals.Fibers;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Scorables.Internals;
namespace O365Bot.Scorables
{
#pragma warning disable 1998
public class CancelScorable : ScorableBase
{
private readonly IDialogTask task;
public CancelScorable(IDialogTask task)
{
SetField.NotNull(out this.task, nameof(task), task);
}
///
/// Compare user input with keyword.
///
protected override async Task PrepareAsync(IActivity activity, CancellationToken token)
{
var message = activity as IMessageActivity;
if (message != null && !string.IsNullOrWhiteSpace(message.Text))
{
if (message.Text.ToLower().Equals("cancel", StringComparison.InvariantCultureIgnoreCase))
{
return message.Text;
}
}
return null;
}
protected override bool HasScore(IActivity item, string state)
{
return state != null;
}
protected override double GetScore(IActivity item, string state)
{
return 1.0;
}
///
/// If keyword found, then reset the current dialog.
///
protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
{
this.task.Reset();
}
protected override Task DoneAsync(IActivity item, string state, CancellationToken token)
{
return Task.CompletedTask;
}
}
}
2. Add GlobalMessageHandlers.cs file in the root and replace the code. In this code, register the CancelScorable.
using Autofac;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Builder.Scorables;
using Microsoft.Bot.Connector;
using O365Bot.Scorables;
namespace O365Bot
{
public class GlobalMessageHandlers : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder
.Register(c => new CancelScorable(c.Resolve()))
.As>()
.InstancePerLifetimeScope();
}
}
}
3. Replace the Global.asax.cs to register the handler on startup. As this is part of Conversation Autofac, using Update method to directly insert the build information.
using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Internals.Fibers;
using O365Bot.Services;
using System.Configuration;
using System.Web.Http;
namespace O365Bot
{
public class WebApiApplication : System.Web.HttpApplication
{
public static IContainer Container;
protected void Application_Start()
{
this.RegisterBotModules();
GlobalConfiguration.Configure(WebApiConfig.Register);
AuthBot.Models.AuthSettings.Mode = ConfigurationManager.AppSettings["ActiveDirectory.Mode"];
AuthBot.Models.AuthSettings.EndpointUrl = ConfigurationManager.AppSettings["ActiveDirectory.EndpointUrl"];
AuthBot.Models.AuthSettings.Tenant = ConfigurationManager.AppSettings["ActiveDirectory.Tenant"];
AuthBot.Models.AuthSettings.RedirectUrl = ConfigurationManager.AppSettings["ActiveDirectory.RedirectUrl"];
AuthBot.Models.AuthSettings.ClientId = ConfigurationManager.AppSettings["ActiveDirectory.ClientId"];
AuthBot.Models.AuthSettings.ClientSecret = ConfigurationManager.AppSettings["ActiveDirectory.ClientSecret"];
var builder = new ContainerBuilder();
builder.RegisterType().As();
Container = builder.Build();
}
private void RegisterBotModules()
{
var builder = new ContainerBuilder();
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule();
builder.Update(Conversation.Container);
}
}
}
Try with emulator
Run the application and try with emulator.
Implement Interruption
What if user wants to see the events while creating one? You can use same global message handler technic.
1. Add GetEventsScorable.cs in Scorables folder and replace code. This is very similar to previous one, but inserting new dialog when the keyword is detected, rather than canceling the current dialog.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Builder.Internals.Fibers;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Scorables.Internals;
using O365Bot.Dialogs;
namespace O365Bot.Scorables
{
#pragma warning disable 1998
public class GetEventsScorable : ScorableBase
{
private readonly IDialogTask task;
public GetEventsScorable(IDialogTask task)
{
SetField.NotNull(out this.task, nameof(task), task);
}
protected override async Task PrepareAsync(IActivity activity, CancellationToken token)
{
var message = activity as IMessageActivity;
if (message != null && !string.IsNullOrWhiteSpace(message.Text))
{
if (message.Text.Equals("get events", StringComparison.InvariantCultureIgnoreCase))
{
return message.Text;
}
}
return null;
}
protected override bool HasScore(IActivity item, string state)
{
return state != null;
}
protected override double GetScore(IActivity item, string state)
{
return 1.0;
}
///
/// If keyword found, then inset dialog
///
protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
{
var message = item as IMessageActivity;
if (message != null)
{
var getEventsDialog = new GetEventsDialog();
var interruption = getEventsDialog.Void();
await this.task.Forward(interruption, null, message, CancellationToken.None);
await this.task.PollAsync(token);
}
}
protected override Task DoneAsync(IActivity item, string state, CancellationToken token)
{
return Task.CompletedTask;
}
}
}
2. Add following method in GlobalMessageHandlers.cs
builder
.Register(c => new GetEventsScorable(c.Resolve()))
.As>()
.InstancePerLifetimeScope();
Try with emulator
Run the application and try with emulator.
Update tests
As I implemented new features, let’s update tests, too.
Unit Test
For unit test, adding Global Message Handler registration method and call it from every test. You need to be careful which Container to register the handler.
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Tests;
using Microsoft.Bot.Connector;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Threading.Tasks;
using Autofac;
using O365Bot.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Builder.Base;
using System.Threading;
using System.Collections.Generic;
using Microsoft.QualityTools.Testing.Fakes;
using O365Bot.Services;
using Moq;
using Microsoft.Graph;
using System.Globalization;
using Microsoft.Bot.Builder.Internals.Fibers;
namespace O365Bot.UnitTests
{
[TestClass]
public class SampleDialogTest : DialogTestBase
{
[TestMethod]
public async Task ShouldReturnEvents()
{
// Instantiate ShimsContext to use Fakes
using (ShimsContext.Create())
{
// Return "dummyToken" when calling GetAccessToken method
AuthBot.Fakes.ShimContextExtensions.GetAccessTokenIBotContextString =
async (a, e) => { return "dummyToken"; };
var mockEventService = new Mock();
mockEventService.Setup(x => x.GetEvents()).ReturnsAsync(new List()
{
new Event
{
Subject = "dummy event",
Start = new DateTimeTimeZone()
{
DateTime = "2017-05-31 12:00",
TimeZone = "Standard Tokyo Time"
},
End = new DateTimeTimeZone()
{
DateTime = "2017-05-31 13:00",
TimeZone = "Standard Tokyo Time"
}
}
});
var builder = new ContainerBuilder();
builder.RegisterInstance(mockEventService.Object).As();
WebApiApplication.Container = builder.Build();
// Instantiate dialog to test
IDialog
Function Test
Simple add following two tests.
[TestMethod]
public void Function_ShouldCancelCurrrentDialog()
{
DirectLineHelper helper = new DirectLineHelper(TestContext);
var toUser = helper.SentMessage("add appointment");
// Verify the result
Assert.IsTrue(toUser[0].Text.Equals("Creating an event."));
Assert.IsTrue(toUser[1].Text.Equals("What is the title?"));
toUser = helper.SentMessage("Learn BotFramework");
Assert.IsTrue(toUser[0].Text.Equals("What is the detail?"));
toUser = helper.SentMessage("Cancel");
Assert.IsTrue(toUser.Count.Equals(0));
toUser = helper.SentMessage("add appointment");
Assert.IsTrue(toUser[0].Text.Equals("Creating an event."));
Assert.IsTrue(toUser[1].Text.Equals("What is the title?"));
}
[TestMethod]
public void Function_ShouldInterruptCurrentDialog()
{
DirectLineHelper helper = new DirectLineHelper(TestContext);
var toUser = helper.SentMessage("add appointment");
// Verify the result
Assert.IsTrue(toUser[0].Text.Equals("Creating an event."));
Assert.IsTrue(toUser[1].Text.Equals("What is the title?"));
toUser = helper.SentMessage("Learn BotFramework");
Assert.IsTrue(toUser[0].Text.Equals("What is the detail?"));
toUser = helper.SentMessage("Get Events");
Assert.IsTrue(true);
toUser = helper.SentMessage("Implement O365Bot");
Assert.IsTrue(toUser[0].Text.Equals("When do you start? Use dd/MM/yyyy HH:mm format."));
}
Checkin the code to make sure all tests are passed.
Summery
Global message handling is one of the key to make intelligent bot. In real scenario, you may want to put several keywords per scorables.