Значения захвата, представленные Adaptive Card в диалоговом окне с водопадом - PullRequest
0 голосов
/ 14 июня 2019

Я последовал совету этого вопроса , комментарию к этой проблеме , а также этому ответу .

Внутри моего водопада Диалог:

  • Отображение адаптивной карты
  • Отправка текстового приглашения сразу после отображения адаптивной карты

Внутри моего основного класса ботов:

  • Установка для свойства Text Activity значения, извлеченного из свойства Value действия, если действие представляет собой сообщение, содержащее данные обратной передачи.

Я пытался использовать AdaptiveTextInput или AdaptiveDateInput в качестве элемента управления для отправки значения, но это не имеет значения. Я чувствую, что это будет что-то действительно глупое ..

В настоящее время я использую в своем водопаде комбинацию карт Hero и Adaptive, карты Hero работают как надо.


Редактировать

Я добавил //! Релевантные комментарии к моему коду для важных частей, остальное оставлено для контекста.

Итак, мой вопрос: что мешает правильной прохождению моего представления на адаптивной карте - это проблема в том, как я отображаюсь в водопаде, проблема в том, как строится действие в карте, или как я обрабатываете действие в основном классе ботов?


Создание моих карт в 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);
        }
    }
}

Внутри моего основного класса ботов:

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);
    }
}

Мой DialogValue, если он вам нужен:

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;
}

Интересно, что моя OnEventAsync функция моего MainDialog (та, которая подключена в Startup.cs через services.AddTransient<IBot, DialogBot<MainDialog>>();) срабатывает, когда я устанавливаю свойство text действия.

1 Ответ

0 голосов
/ 14 июня 2019

Моя проблема оказалась двукратной


1) Внутри моего OnTurnAsync метода в моем DialogBot файле, который у меня был:

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

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

Я устанавливал свойство Text переменной postBackActivity вместо прямой установки свойства Text непосредственно на dc.Context.Activity. Поскольку я отправлял переменную через SendActivityAsync, она скрывала эту ошибку, потому что я получал нужное значение, переданное методу OnEventAsync в моем классе MainDialog.

Правильный способ был установить это непосредственно в контексте, а не в его копии (DOH!)

dc.Context.Activity.Text = text

2) Внутри метода OnEventAsync в моем классе MainDialog у меня был пустой блок, который перехватывал ответ, но ничего с ним не делал (нужно было вызвать await dc.ContinueDialogAsync()). Однако это уже было обработано существующим блоком кода в шаблоне Virtual Assistant, который был заблокирован моим пустым блоком.

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, он провалился до необходимого кода (передняя часть).


Список изменений:

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 и должен был быть более подозрительным к последнему пункту моего вопроса:

Интересно, что моя функция OnEventAsync моего MainDialog (та, которая подключается в Startup.cs через services.AddTransient> ();), срабатывает, когда я устанавливаю свойство text действия.

Так близко, но так далеко. Надеюсь, это поможет кому-то еще в будущем.

Ссылка, которую я нашел полезной, была:

...