Botframework-sdk: How to get the IDialogContext object in global message handler?

Created on 25 Apr 2018  路  5Comments  路  Source: microsoft/botframework-sdk

Bot Info

  • SDK Platform: .NET
  • SDK Version: 3.12.2.4
  • Active Channels: Telegram
  • Deployment Environment: Azure Bot Service

Issue Description

Hi!

There is a global message handler created from the example in the documentation (https://docs.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-scorable-dialogs).

There is a nesessary for a certain message to show the confirmation dialog. As a confirmatory dialogue, I wanted to use the PromptDialog.Confirm(...). But the first parameter of PromptDialog.Confirm() is IDialogContext object.
Can somehow get an IDialogContext object in this case?

Most helpful comment

You can send an IDialogContext to your scorable like this:

Conversation.UpdateContainer(builder =>
{
 builder
      .Register(c =>
     {
          var contextFunc = c.Resolve<Func<IDialogStack, CancellationToken, IDialogContext>>();
          return new TestScorable(contextFunc(c.Resolve<IDialogStack>(), CancellationToken.None));
     })
     .As<IScorable<IActivity, double>>()
     .InstancePerLifetimeScope();
});

I don't know how to use a Prompt from within the scorable, but you can add your own logic to mimic the behavior. Something like this TestScorable:

    [Serializable]
    public class TestScorable : ScorableBase<IActivity, string, double>
    {
        private readonly IDialogContext dialogContext;

        public TestScorable(IDialogContext dialogContext)
        {
            SetField.NotNull(out this.dialogContext, nameof(dialogContext), dialogContext);
        }

        protected override async Task<string> PrepareAsync(IActivity activity, CancellationToken token)
        {
            var message = activity as IMessageActivity;
            if (message != null && message.Text != null)
            {
                bool? insideTest;
                if (dialogContext.PrivateConversationData.TryGetValue<bool?>("ResumeInTest", out insideTest))
                {
                    if (insideTest.HasValue && insideTest.Value)
                    {
                        dialogContext.PrivateConversationData.RemoveValue("ResumeInTest");

                        if (message.Text.ToLower().StartsWith("y"))
                            await dialogContext.PostAsync("okay, running the test");
                        else
                            await dialogContext.PostAsync("okay, cancelling the test");                        
                    }
                    return "done with test";
                }

                if (message.Text.ToLower() == "test")
                {
                    var text = message.Text;
                    dialogContext.PrivateConversationData.SetValue("ResumeInTest", true);
                    await dialogContext.PostAsync("Would you really like to do this test?");
                    return "testing";
                }
            }
            return null;
        }

        private Task AfterConfirm(IDialogContext context, IAwaitable<bool> result)
        {
            return Task.CompletedTask;
        }

        protected override bool HasScore(IActivity item, string state)
        {
            return state != null;
        }

        protected override double GetScore(IActivity item, string state)
        {
            return 1.0;
        }

        protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
        {

        }

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

I just added all of the logic to PrepareAsync, but normally you would probably want it in PostAsync, and just setup code in PrepareAsync.

All 5 comments

I'd love to know if there's a simple way of doing this. Currently I'm just creating a new dialog for each scorable to call so that I can use the IDialogContext there.

You can send an IDialogContext to your scorable like this:

Conversation.UpdateContainer(builder =>
{
 builder
      .Register(c =>
     {
          var contextFunc = c.Resolve<Func<IDialogStack, CancellationToken, IDialogContext>>();
          return new TestScorable(contextFunc(c.Resolve<IDialogStack>(), CancellationToken.None));
     })
     .As<IScorable<IActivity, double>>()
     .InstancePerLifetimeScope();
});

I don't know how to use a Prompt from within the scorable, but you can add your own logic to mimic the behavior. Something like this TestScorable:

    [Serializable]
    public class TestScorable : ScorableBase<IActivity, string, double>
    {
        private readonly IDialogContext dialogContext;

        public TestScorable(IDialogContext dialogContext)
        {
            SetField.NotNull(out this.dialogContext, nameof(dialogContext), dialogContext);
        }

        protected override async Task<string> PrepareAsync(IActivity activity, CancellationToken token)
        {
            var message = activity as IMessageActivity;
            if (message != null && message.Text != null)
            {
                bool? insideTest;
                if (dialogContext.PrivateConversationData.TryGetValue<bool?>("ResumeInTest", out insideTest))
                {
                    if (insideTest.HasValue && insideTest.Value)
                    {
                        dialogContext.PrivateConversationData.RemoveValue("ResumeInTest");

                        if (message.Text.ToLower().StartsWith("y"))
                            await dialogContext.PostAsync("okay, running the test");
                        else
                            await dialogContext.PostAsync("okay, cancelling the test");                        
                    }
                    return "done with test";
                }

                if (message.Text.ToLower() == "test")
                {
                    var text = message.Text;
                    dialogContext.PrivateConversationData.SetValue("ResumeInTest", true);
                    await dialogContext.PostAsync("Would you really like to do this test?");
                    return "testing";
                }
            }
            return null;
        }

        private Task AfterConfirm(IDialogContext context, IAwaitable<bool> result)
        {
            return Task.CompletedTask;
        }

        protected override bool HasScore(IActivity item, string state)
        {
            return state != null;
        }

        protected override double GetScore(IActivity item, string state)
        {
            return 1.0;
        }

        protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
        {

        }

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

I just added all of the logic to PrepareAsync, but normally you would probably want it in PostAsync, and just setup code in PrepareAsync.

@MRowley1,
I tried to start a new dialog, but ran into a problem.

[Serializable]
class ResetDialog : IDialog<bool>
{
    public async Task StartAsync(IDialogContext context)
    {
        string[][] patterns = new string[2][];
        patterns[0] = new string[] { "yes" };
        patterns[1] = new string[] { "no" };

        string[] options = new string[] { "Yes", "No" };

        PromptDialog.Confirm(
            context,
            Done,
            "Reset?",
            options: options,
            patterns: patterns,
            retry: "Push the button"
        );
    }

    private async Task Done(IDialogContext context, IAwaitable<bool> result)
    {
        bool done = await result;
        context.Done(true);
    }
}

// ...

public class GlobalCommandsHandler : ScorableBase<IActivity, string, double>
{
    public GlobalCommandsHandler(IDialogTask task)
    {
        //...
    }

    public override async Task PostAsync(IDialogTask task, IActivity item, string state, CancellationToken token)
    {
        task.Call(new ResetDialog(), async (IDialogContext context, IAwaitable<bool> result) => // !!! Exeption !!!
        {
            bool res = await result;
            if (res)
            {
                //...
                task.Reset();
            }
        }
        );
    }
}

Exception: Microsoft.Bot.Builder.Internals.Fibers.InvalidNeedException: "invalid need: expected Call, have Wait"
Can you show your version?

@EricDahlvang,

You can send an IDialogContext to your scorable like this

Your example is working, I checked.
But in my Scorable I need IDialogContext in addition to the IDialogTask: through the IDialogTask, I do task.Reset() and the IDialogContext I need to try to call PromptDialog. That is, I need to pass both parameters to the Scorable:

public GlobalMessageHandler(IDialogTask task, IDialogContext context)
{
    //...
}

But I do not understand anything about using Autofac (and little about dependency injection in particular), so I can not apply your example to my situation now :(
I'll go read something on this theme.

I don't know how to use a Prompt from within the scorable, but you can add your own logic to mimic the behavior. Something like this TestScorable

In my case, there is one global message handler that calls specific subclasses of handlers depending on the message:

public class ResetDialogStack : GlobalCommandBase
{

    // ...

    public override async Task PostAsync(IDialogTask task, IActivity item, string state, CancellationToken token)
    {
        //...
        task.Reset();
    }
}

public class GlobalCommandsHandler : ScorableBase<IActivity, string, double>
{
    private IDialogTask task;

    private List<GlobalCommandBase> _allGlobalCommands;
    private List<GlobalCommandBase> _executingCommands;

    public GlobalCommandsHandler(IDialogTask task)
    {
        SetField.NotNull(out this.task, nameof(task), task);

        _allGlobalCommands = new List<GlobalCommandBase>();
        _allGlobalCommands.Add(new ResetDialogStack());
    }

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

        if (message == null || string.IsNullOrWhiteSpace(message.Text))
            return null;

        _executingCommands = _allGlobalCommands
                                .Where(c => c.Contains(message.Text))
                                .Where(c => c.AdditionalCondition(task))
                                .ToList();

        return _executingCommands.Count > 0 ? message.Text : null;
    }

    protected override bool HasScore(IActivity item, string state)
    {
        return state != null;
    }

    protected override double GetScore(IActivity item, string state)
    {
        return 1.0;
    }

    protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
    {
        foreach (var command in _executingCommands)
            await command.PostAsync(task, item, state, token);
    }

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

Therefore, I would not like to touch it or write some business logic in it for some specific scenario.

I believe it was based on one of the samples, but the equivalent of the code I'm using would be

public override async Task PostAsync(IDialogTask task, IActivity item, string state, CancellationToken token)
{
    //Create new dialog
    var resetDialog = new ResetDialog();
    var interruption = resetDialog.Void<object, IMessageActivity>();

    //Add to stack and call StartAsync
    task.Call(interruption, null);
    await task.PollAsync(token);
}

I would suggest trying to get Eric's solution working first before using this as a workaround though.

Closing this issue. If you require further assistance, please open a new issue or ask a question on Stack Overflow using the BotFramework tag https://stackoverflow.com/questions/tagged/botframework

Was this page helpful?
0 / 5 - 0 ratings

Related issues

daveta picture daveta  路  3Comments

hailiang-wang picture hailiang-wang  路  3Comments

sebsylvester picture sebsylvester  路  3Comments

mattlanham picture mattlanham  路  3Comments

somprabhsharma picture somprabhsharma  路  3Comments