在瀑布对话框中捕获自适应卡提交的值 [英] Capture values submitted by Adaptive Card in waterfall dialog

查看:62
本文介绍了在瀑布对话框中捕获自适应卡提交的值的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已遵循此问题的评论以及此答案.

在瀑布"对话框中:

  • 显示自适应卡
  • 显示自适应卡后立即发送文本提示

在我的主要机器人课堂中:

Inside my main bot class:

  • 如果活动是包含回发数据的消息,则将ActivityText属性设置为从活动的Value属性提取的值的属性.
  • Setting the Text property of the Activity to that of the extracted value from the Value property of the activity if the activity is a message that contains postback data.

我尝试使用AdaptiveTextInputAdaptiveDateInput作为控件来提交值,但没有任何区别.我觉得这将是一件非常愚蠢的事情.

I have tried using a AdaptiveTextInput or AdaptiveDateInput as the control to submit the value but it doesn't make a difference. I feel like it's going to be something really silly..

我目前在瀑布中混合使用HeroAdaptive卡,Hero卡可以正常工作.

I am currently using a mix of Hero and Adaptive cards in my waterfall, the Hero cards are working as they should.

修改

我在代码中的//重要部分添加了//!relevant-注释,其余部分留给上下文使用.

I have added // !Relevant- comments to my code to the important parts, the rest is left for context.

所以我的问题是:是什么阻止我在自适应卡上的提交正确地通过-是我在瀑布中的显示方式方面的问题,卡中动作的构造方式还是我的问题?在机器人主类中处理动作吗?

So my question is: what is preventing my submit on the adaptive card from flowing through correctly - is it a problem in how I am displaying in the waterfall, a problem with how the action is constructed in the card, or how I am handling the action in the main bot class?

AdaptiveCardService中构建我的卡:

public List<Activity> BuildCardActivitiesFromDecisionFlow(BotDecisionFlow botDecisionFlow)
{
    List<Activity> cardActivities = new List<Activity>();

    foreach (Step step in botDecisionFlow.FormSchema.Steps)
    {
        Control control = step.Details.Control;

        cardActivities.Add(CreateCardActivity(step, control));
    }

    return cardActivities;
}

private Activity CreateCardActivity(Step step, Control control)
{
    Activity cardActivity = (Activity)Activity.CreateMessageActivity();

    if (control.Type == ControlTypeEnum.RadioButton)
    {
        HeroCard heroCard = BuildHeroCard(step, control.DataType);
        Attachment attachment = heroCard.ToAttachment();

        cardActivity.Attachments.Add(attachment);
    }
    else if (control.Type == ControlTypeEnum.DatePicker)
    {
        AdaptiveCard adaptiveCard = BuildAdaptiveCard(step, control.DataType);

        Attachment attachment = new Attachment
        {
            ContentType = AdaptiveCard.ContentType,
            // Trick to get Adapative Cards to work with prompts as per https://github.com/Microsoft/botbuilder-dotnet/issues/614#issuecomment-443549810
            Content = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(adaptiveCard))
        };

        cardActivity.Attachments.Add(attachment);
    }
    else
    {
        throw new NotImplementedException($"The {nameof(control)} with value {control} is not yet supported.");
    }

    return cardActivity;
}

private HeroCard BuildHeroCard(Step step, DataTypeEnum dataType)
{
    string question = step.Details.Question;

    HeroCard heroCard = new HeroCard
    {
        Text = question,
        // PostBack is required to get buttons to work with prompts, also the value needs to be a string for the
        // event to fire properly, as per https://stackoverflow.com/a/56297792/5209435
        Buttons = step.Details.EnumValueToDisplayTextMappings.Select(e => new CardAction(ActionTypes.PostBack, e.Value, null, e.Value, e.Value, JsonConvert.SerializeObject(new DialogValueDto(step.Name, e.Key, dataType)), null)).ToList()
    };

    return heroCard;
}

private AdaptiveCard BuildAdaptiveCard(Step step, DataTypeEnum dataType)
{
    const string ISO8601Format = "yyyy-MM-dd";
    string question = step.Details.Question;

    DateTime today = DateTime.Today;
    string todayAsIso = today.ToString(ISO8601Format);

    AdaptiveCard adaptiveCard = new AdaptiveCard("1.0")
    {
        Body =
        {
            new AdaptiveContainer
            {
                Items =
                {
                    new AdaptiveTextBlock
                    {
                        Text = question,
                        Wrap = true
                    },
                    new AdaptiveDateInput
                    {
                        Id = "UserInput",
                        Value = todayAsIso,
                        Min = today.AddDays(-7).ToString(ISO8601Format),
                        Max = todayAsIso,
                        Placeholder = todayAsIso
                    }
                }
            }
        },
        Actions = new List<AdaptiveAction>
        {
            // !Relevant-Start
            new AdaptiveSubmitAction
            {
                Data = new DialogValueDto(step.Name, dataType),
                Title = "Confirm",
                Type = "Action.Submit"
            }
            // !Relevant-End
        }
    };

    return adaptiveCard;
}

在我的瀑布课中:

private readonly IUmbracoApiWrapper _umbracoApiWrapper;
    private readonly IUmbracoResponseConverterService _umbracoResponseConverterService;
    private readonly IAdaptiveCardService _adaptiveCardService;

    private IStatePropertyAccessor<DynamicWaterfallState> _accessor;
    private DynamicWaterfallState _state;

    public DynamicWaterfallDialog(
        IUmbracoApiWrapper umbracoApiWrapper,
        IUmbracoResponseConverterService umbracoResponseConverterService,
        IAdaptiveCardService adaptiveCardService,
        UserState userState)
        : base(nameof(DynamicWaterfallDialog))
    {
        _accessor = userState.CreateProperty<DynamicWaterfallState>(nameof(DynamicWaterfallState));
        _umbracoApiWrapper = umbracoApiWrapper;
        _umbracoResponseConverterService = umbracoResponseConverterService;
        _adaptiveCardService = adaptiveCardService;

        InitialDialogId = nameof(WaterfallDialog);

        // !Relevant-Start
        var waterfallSteps = new WaterfallStep[]
        {
            // TODO: Rename this DisplayCardAsync
            UserInputStepAsync,
            // TODO: Rename this ProcessCardAsync
            LoopStepAsync,
        };

        AddDialog(new TextPrompt(nameof(TextPrompt)));
        AddDialog(new WaterfallDialog(InitialDialogId, waterfallSteps));
        // !Relevant-End
    }

    // TODO: Does it make more sense for the collection of dialogs to be passed in? It depends on how this dialog is going to be called, 
    // maybe just passing in the ID is fine rather than having code sprinkled around to fetch the dialog collections.
    public async Task<DialogTurnResult> UserInputStepAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
    {
        // Get passed in options, need to serialise the object before we deserialise because calling .ToString on the object is unreliable
        string tempData = JsonConvert.SerializeObject(sc.Options);
        DynamicWaterfallDialogDto dynamicWaterfallDialogDto = JsonConvert.DeserializeObject<DynamicWaterfallDialogDto>(tempData);

        // Read out data from the state
        _state = await _accessor.GetAsync(sc.Context, () => new DynamicWaterfallState());

        List<Activity> activityCards = _state.ActivityDialogs ?? new List<Activity>();
        int dialogPosition = _state.DialogPosition;
        bool flowFinished = _state.FlowFinished;
        bool apiDataFetched = _state.ApiDataFetched;

        if (DynamicWaterfallDialogDtoExtensions.IsDynamicWaterfallDialogDtoValid(dynamicWaterfallDialogDto) && !apiDataFetched)
        {
            // Fetch from API
            JObject decision = await _umbracoApiWrapper.GetDecisionById(18350);

            UmbracoDecisionResponseDto umbracoResponseDto = JsonConvert.DeserializeObject<UmbracoDecisionResponseDto>(decision.ToString());

            BotDecisionFlow botDecisionFlow = new BotDecisionFlow(_umbracoResponseConverterService, umbracoResponseDto);

            activityCards = _adaptiveCardService.BuildCardActivitiesFromDecisionFlow(botDecisionFlow);

            _state.ApiDataFetched = true;
            _state.ActivityDialogs = activityCards;

            await _accessor.SetAsync(sc.Context, _state, cancellationToken);
        }

        var cardToShow = activityCards.ElementAt(dialogPosition);

        _state.FlowFinished = _state.DialogPosition == activityCards.Count - 1;
        _state.DialogPosition++;

        await _accessor.SetAsync(sc.Context, _state, cancellationToken);

        // TODO we need to determine the control type to figure out the prompt type?

        // !Relevant-Start
        await sc.Context.SendActivityAsync(cardToShow);
        return await sc.PromptAsync(nameof(TextPrompt), new PromptOptions() { Prompt = MessageFactory.Text("") });
        //return await sc.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = new Activity { Type = ActivityTypes.Message } });
        // !Relevant-End
    }

    public async Task<DialogTurnResult> LoopStepAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
    {
        object result = sc.Result;
        DialogValueDto userInput = JsonConvert.DeserializeObject<DialogValueDto>(sc.Result.ToString());

        await sc.Context.SendActivityAsync($"You selected: {userInput.UserInput}");

        _state = await _accessor.GetAsync(sc.Context, () => new DynamicWaterfallState());

        bool flowFinished = _state.FlowFinished;

        // TODO: Do we want to do state manipulation in here?

        if (!flowFinished)
        {
            // TODO: Do we want to pass in custom options here?
            return await sc.ReplaceDialogAsync(nameof(DynamicWaterfallDialog), sc.Options, cancellationToken);
        }
        else
        {
            // TODO: We probably want to pass the state in here instead of null if we want to show outcomes etc
            return await sc.EndDialogAsync(null, cancellationToken);
        }
    }
}

在我的主要机器人课堂中:

Inside my main bot class:

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
    // Client notifying this bot took to long to respond (timed out)
    if (turnContext.Activity.Code == EndOfConversationCodes.BotTimedOut)
    {
        _telemetryClient.TrackTrace($"Timeout in {turnContext.Activity.ChannelId} channel: Bot took too long to respond.", Severity.Information, null);
        return;
    }

    var dc = await _dialogs.CreateContextAsync(turnContext);

    // !Relevant-Start
    if (turnContext.Activity.Type == ActivityTypes.Message)
    {
        // Ensure that message is a postBack (like a submission from Adaptive Cards)
        if (dc.Context.Activity.GetType().GetProperty("ChannelData") != null)
        {
            var channelData = JObject.Parse(dc.Context.Activity.ChannelData.ToString());

            // TODO: Add check for type, we should only handle adaptive cards here
            if (channelData.ContainsKey("postBack"))
            {
                var postbackActivity = dc.Context.Activity;

                string text = JsonConvert.DeserializeObject<DialogValueDto>(postbackActivity.Value.ToString())?.UserInput;


                // Convert the user's Adaptive Card input into the input of a Text Prompt
                // Must be sent as a string
                postbackActivity.Text = text;
                await dc.Context.SendActivityAsync(postbackActivity);
            }
        }
    }
    // !Relevant-End

    if (dc.ActiveDialog != null)
    {
        var result = await dc.ContinueDialogAsync();
    }
    else
    {
        await dc.BeginDialogAsync(typeof(T).Name);
    }
}

我的DialogValueD,以备不时之需:

My DialogValueDto incase you need it:

public string StepName { get; set; }
public string UserInput { get; set; }
public DataTypeEnum DataType { get; set; }

/// <summary>
/// For JSON deserialization
/// </summary>
public DialogValueDto()
{
}

/// <summary>
/// For use with DateTime deserialization.
/// The control id is set to "UserInput"
/// so this property will be set automatically
/// </summary>
public DialogValueDto(string stepName, DataTypeEnum dataType)
{
    StepName = stepName;
    DataType = dataType;
}

/// <summary>
/// This is the constructor that should be used most
/// of the time
/// </summary>
public DialogValueDto(string stepName, string userInput, DataTypeEnum dataType)
{
    StepName = stepName;
    UserInput = userInput;
    DataType = dataType;
}

当我设置活动的text属性时,我的MainDialog的OnEventAsync函数(通过services.AddTransient<IBot, DialogBot<MainDialog>>();连接到Startup.cs中的那个)足够有趣的了.

Interestingly enough my OnEventAsync function of my MainDialog (the one which is wired up in Startup.cs via services.AddTransient<IBot, DialogBot<MainDialog>>();) gets fired when I set the text property of the activity.

推荐答案

我的问题原来是双重的

1)在我的DialogBot文件中的OnTurnAsync方法中,我拥有:

1) Inside my OnTurnAsync method in my DialogBot file I had:

var postbackActivity = dc.Context.Activity;
string text = JsonConvert.DeserializeObject<DialogValueDto>(postbackActivity.Value.ToString())?.UserInput;

postbackActivity.Text = text;
await dc.Context.SendActivityAsync(postbackActivity);

我正在设置postBackActivity变量的Text属性,而不是直接在dc.Context.Activity上直接设置Text属性.因为我通过SendActivityAsync发送变量,所以它掩盖了这个错误,因为我获得了想要传递给MainDialog类中OnEventAsync方法的值.

I was setting the Text property of postBackActivity variable instead of directly setting the Text property directly on dc.Context.Activity. Because I was sending the the variable through SendActivityAsync it was covering up this mistake because I was getting the value I wanted passed through to the OnEventAsync method in my MainDialog class.

正确的方法是直接在上下文上设置它,而不是在副本上设置(DOH!)

The correct way was to set this directly on the context, not on a copy of it (DOH!)

dc.Context.Activity.Text = text


2)在我的MainDialog类的OnEventAsync方法内部,我有一个空块,该块捕获了响应,但未执行任何操作(需要调用await dc.ContinueDialogAsync()).但是,这已经由Virtual Assistant模板中的现有代码块进行了处理,而我的空代码块阻止了该代码块被击中.


2) Inside the OnEventAsync method in my MainDialog class I had an empty block which caught the response but did nothing with it (it needed to call await dc.ContinueDialogAsync()). However, this was already handled by an existing block of code in the Virtual Assistant template which my empty block was preventing from being hit.

object value = dc.Context.Activity.Value;

if (condition)
{
    // do nothing
}
else if (value.GetType() == typeof(JObject))
{
    // code from the Virtual Assistant template to check the values passed through
    var submit = JObject.Parse(value.ToString());

    // more template code

    // Template code
    if (forward)
    {
        var result = await dc.ContinueDialogAsync();

        if (result.Status == DialogTurnStatus.Complete)
        {
            await CompleteAsync(dc);
        }
    }
}

一旦我删除了空的if块,它就进入了所需的代码(正向部分).

Once I removed my empty if block then it fell through to the code it needed (the forward part).

更改列表:

DynamicWaterfallDialog:

public DynamicWaterfallDialog(
    ...
    )
    : base(nameof(DynamicWaterfallDialog))
{
    ...

    InitialDialogId = nameof(WaterfallDialog);

    var waterfallSteps = new WaterfallStep[]
    {
        UserInputStepAsync,
        LoopStepAsync,
    };

    AddDialog(new TextPrompt(nameof(TextPrompt)));
    AddDialog(new WaterfallDialog(InitialDialogId, waterfallSteps));
}

DialogBot:

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
    ...

    var dc = await _dialogs.CreateContextAsync(turnContext);

    if (dc.Context.Activity.Type == ActivityTypes.Message)
    {
        // Ensure that message is a postBack (like a submission from Adaptive Cards)
        if (dc.Context.Activity.GetType().GetProperty("ChannelData") != null)
        {
            JObject channelData = JObject.Parse(dc.Context.Activity.ChannelData.ToString());
            Activity postbackActivity = dc.Context.Activity;

            if (channelData.ContainsKey("postBack") && postbackActivity.Value != null)
            {
                DialogValueDto dialogValueDto = JsonConvert.DeserializeObject<DialogValueDto>(postbackActivity.Value.ToString());

                // Only set the text property for adaptive cards because the value we want, and the value that the user submits comes through the
                // on the Value property for adaptive cards, instead of the text property like everything else
                if (DialogValueDtoExtensions.IsValidDialogValueDto(dialogValueDto) && dialogValueDto.CardType == CardTypeEnum.Adaptive)
                {
                    // Convert the user's Adaptive Card input into the input of a Text Prompt, must be sent as a string
                    dc.Context.Activity.Text = JsonConvert.SerializeObject(dialogValueDto);

                    // We don't need to post the text as per https://stackoverflow.com/a/56010355/5209435 because this is automatically handled inside the
                    // OnEventAsync method of MainDialog.cs
                }
            }
        }
    }

    if (dc.ActiveDialog != null)
    {
        var result = await dc.ContinueDialogAsync();
    }
    else
    {
        await dc.BeginDialogAsync(typeof(T).Name);
    }
}

MainDialog:

protected override async Task OnEventAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
    object value = dc.Context.Activity.Value;

    if (value.GetType() == typeof(JObject))
    {
        var submit = JObject.Parse(value.ToString());
        if (value != null)
        {
            // Null propagation here is to handle things like dynamic adaptive cards that submit objects
            string action = submit["action"]?.ToString();

            ...
        }

        var forward = true;
        var ev = dc.Context.Activity.AsEventActivity();

        // Null propagation here is to handle things like dynamic adaptive cards that may not convert into an EventActivity
        if (!string.IsNullOrWhiteSpace(ev?.Name))
        {
            ...
        }

        if (forward)
        {
            var result = await dc.ContinueDialogAsync();

            if (result.Status == DialogTurnStatus.Complete)
            {
                await CompleteAsync(dc);
            }
        }
    }
}

我想我期望在上下文上设置Text属性会自动触发到我的LoopStepAsync(DynamicWaterfallDialog)处理程序,而不是掉入OnEventAsync(MainDialog).我知道我需要在某个地方打电话给ContinueDialogAsync,应该对我的问题的最后一段更加怀疑:

I guess I was expecting having the Text property set on the context to automatically fire through to my LoopStepAsync (DynamicWaterfallDialog) handler rather than falling into OnEventAsync (MainDialog). I knew I needed to call ContinueDialogAsync somewhere and should have been more suspicious of the final paragraph of my question:

当我设置活动的text属性时,会激发我的MainDialog的OnEventAsync函数(通过services.AddTransient>()在Startup.cs中连接的函数)足够有趣.

Interestingly enough my OnEventAsync function of my MainDialog (the one which is wired up in Startup.cs via services.AddTransient>();) gets fired when I set the text property of the activity.

那么近,到目前为止.希望这可以在将来帮助其他人.

So close, yet so far. Hopefully this helps someone else out in the future.

我认为有用的链接是:

  • ComplexDialogBot.cs.
  • Question about adaptive cards and waterfalls.
  • GitHub issue about Adaptive Cards and prompts.

这篇关于在瀑布对话框中捕获自适应卡提交的值的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆