Tutorial: Enable proactive notifications

Set up the Virtual Assistant

Option: Using the Enterprise Assistant sample

The Enterprise Assistant sample is already configured with the Proactive State Middleware necessary event handling. Continue to the next step.

Option: Using the core Virtual Assistant Template

Add the Proactive State Middleware

For messages to be delivered to a user’s conversation, a ConversationReference needs to be persisted in the Virtual Assistant and used to resume the existing conversation.

Update both the Startup and DefaultAdapter classes with references to ProactiveState and ProactiveStateMiddleware.

Startup.cs

 public void ConfigureServices(IServiceCollection services)
{
...
+ services.AddSingleton<ProactiveState>();
...
}

DefaultAdapter.cs

public DefaultAdapter(
            BotSettings settings,
            TemplateEngine templateEngine,
            ConversationState conversationState,
            ICredentialProvider credentialProvider,
            TelemetryInitializerMiddleware telemetryMiddleware,
            IBotTelemetryClient telemetryClient,
+           ProactiveState proactiveState)
            : base(credentialProvider)
        {
            OnTurnError = async (turnContext, exception) =>
            {
                await turnContext.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"{exception.Message}"));
                await turnContext.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"{exception.StackTrace}"));
                await turnContext.SendActivityAsync(templateEngine.EvaluateTemplate("errorMessage"));
                telemetryClient.TrackException(exception);
            };

            Use(telemetryMiddleware);

            // Uncomment the following line for local development without Azure Storage
            // Use(new TranscriptLoggerMiddleware(new MemoryTranscriptStore()));
            Use(new TranscriptLoggerMiddleware(new AzureBlobTranscriptStore(settings.BlobStorage.ConnectionString, settings.BlobStorage.Container)));
            Use(new ShowTypingMiddleware());
            Use(new FeedbackMiddleware(conversationState, telemetryClient));
            Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us"));
            Use(new EventDebuggerMiddleware());
+           Use(new ProactiveStateMiddleware(proactiveState));
        }

Handle the BroadcastEvent activity

The Event Handler sends a BroadcastEvent activity that must be handled by the Virtual Assistant. The _proactiveStateAccessor contains the mapping between a user id and a previous conversation.

Update the MainDialog class with the below changes to the constructor and OnEventActivityAsync method.

DefaultActivityHandler.cs

public class DefaultActivityHandler<T> : TeamsActivityHandler
    where T : Dialog
{
    private readonly Dialog _dialog;
    private readonly BotState _conversationState;
    private readonly BotState _userState;
    private IStatePropertyAccessor<DialogState> _dialogStateAccessor;
    private IStatePropertyAccessor<UserProfileState> _userProfileState;
    private LocaleTemplateManager _templateManager;
+   private MicrosoftAppCredentials _appCredentials;
+   private IStatePropertyAccessor<ProactiveModel> _proactiveStateAccessor;

    public DefaultActivityHandler(
        IServiceProvider serviceProvider,
        IBotTelemetryClient telemetryClient,
+       MicrosoftAppCredentials appCredentials,
+       ProactiveState proactiveState,
        T dialog)
    {
        _dialog = dialog;
        _conversationState = serviceProvider.GetService<ConversationState>();
        _userState = serviceProvider.GetService<UserState>();
        _dialogStateAccessor = _conversationState.CreateProperty<DialogState>(nameof(DialogState));
        _userProfileState = _userState.CreateProperty<UserProfileState>(nameof(UserProfileState));
        _templateManager = serviceProvider.GetService<LocaleTemplateManager>();
+       _appCredentials = appCredentials;
+       _proactiveStateAccessor = proactiveState.CreateProperty<ProactiveModel>(nameof(ProactiveModel));
    ...

    protected override async Task OnEventActivityAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
    {
    ...
+   case "BroadcastEvent":
+       {
+           var eventData = JsonConvert.DeserializeObject<EventData>(innerDc.Context.Activity.Value.ToString());
+           var proactiveModel = await _proactiveStateAccessor.GetAsync(innerDc.Context, () => new ProactiveModel());
+           var hashedUserId = MD5Util.ComputeHash(eventData.UserId);
+           var conversationReference = proactiveModel[hashedUserId].Conversation;
+
+           await innerDc.Context.Adapter.ContinueConversationAsync(_appCredentials.MicrosoftAppId, conversationReference, ContinueConversationCallback(innerDc.Context, eventData.Message), cancellationToken);
+           break;
+       }
    ...
+    /// <summary>
+    /// Continue the conversation callback.
+    /// </summary>
+    /// <param name="context">Turn context.</param>
+    /// <param name="message">Activity text.</param>
+    /// <returns>Bot Callback Handler.</returns>
+    private BotCallbackHandler ContinueConversationCallback(ITurnContext context, string message)
+    {
+        return async (turnContext, cancellationToken) =>
+        {
+            var activity = turnContext.Activity.CreateReply(message);
+            EnsureActivity(activity);
+            await turnContext.SendActivityAsync(activity);
+        };
+    }
+
+    /// <summary>
+    /// This method is required for proactive notifications to work in Web Chat.
+    /// </summary>
+    /// <param name="activity">Proactive Activity.</param>
+    private void EnsureActivity(Activity activity)
+    {
+        if (activity != null)
+        {
+            if (activity.From != null)
+            {
+                activity.From.Name = "User";
+                activity.From.Properties["role"] = "user";
+            }
+
+            if (activity.Recipient != null)
+            {
+                activity.Recipient.Id = "1";
+                activity.Recipient.Name = "Bot";
+                activity.Recipient.Properties["role"] = "bot";
+            }
+        }
+    }
   ...
}

EventData.cs

Add a new class named EventData with the following properties.

+    public class EventData
+    {
+        public string UserId { get; set; }

+        public string Message { get; set; }
+    }

NOTE

After you’ve made the above changes to your bot, please be sure to deploy the latest bot code to it’s hosted Azure Web App. The Proactive Notification will only work on an Azure bot over the Direct Line channel (a locally-hosted bot is unable to receive the notifications).