Skip to content

Commit

Permalink
[#6086] Teams is adding support for suggested actions in 1-1 chats (#…
Browse files Browse the repository at this point in the history
…6607)

* Add support for Teams suggested actions

* Add unit tests

* Overload methods to avoid breaking compatibility
  • Loading branch information
ceciliaavila authored May 16, 2023
1 parent 0cda584 commit 5549586
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,11 @@ protected override async Task<IActivity> OnRenderPromptAsync(DialogContext dc, I
var opts = await GetChoiceOptionsAsync(dc, locale).ConfigureAwait(false);
var choiceOptions = opts ?? DefaultChoiceOptions[locale];
var options = dc.State.GetValue<ChoiceInputOptions>(ThisPath.Options);
var conversationType = dc.Context.Activity.Conversation?.ConversationType;
var recipient = dc.Context.Activity.From?.Id;
var toList = string.IsNullOrEmpty(recipient) ? null : new List<string>() { recipient };

return AppendChoices(prompt.AsMessageActivity(), channelId, options.Choices, Style.GetValue(dc.State), choiceOptions);
return AppendChoices(prompt.AsMessageActivity(), channelId, options.Choices, Style.GetValue(dc.State), choiceOptions, conversationType, toList);
}

private async Task<ChoiceSet> GetChoiceSetAsync(DialogContext dc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ protected override async Task<IActivity> OnRenderPromptAsync(DialogContext dc, I

var prompt = await base.OnRenderPromptAsync(dc, state, cancellationToken).ConfigureAwait(false);
var (style, _) = Style.TryGetValue(dc.State);
return AppendChoices(prompt.AsMessageActivity(), channelId, confirmChoices, style, choiceOptions);
var conversationType = dc.Context.Activity.Conversation?.ConversationType;
var recipient = dc.Context.Activity.From?.Id;
var toList = string.IsNullOrEmpty(recipient) ? null : new List<string>() { recipient };
return AppendChoices(prompt.AsMessageActivity(), channelId, confirmChoices, style, choiceOptions, conversationType, toList);
}

private string DetermineCulture(DialogContext dc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,23 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
/// <param name="cancellationToken">cancellation Token.</param>
/// <returns>bound activity ready to send to the user.</returns>
protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, CancellationToken cancellationToken = default(CancellationToken))
{
return AppendChoices(prompt, channelId, choices, style, options, null, null, cancellationToken);
}

/// <summary>
/// AppendChoices is utility method to build up a message activity given all of the options.
/// </summary>
/// <param name="prompt">prompt.</param>
/// <param name="channelId">channelId.</param>
/// <param name="choices">choices to present.</param>
/// <param name="style">listType.</param>
/// <param name="options">options to control the choice rendering.</param>
/// <param name="conversationType">the type of the conversation.</param>
/// <param name="toList">the list of recipients.</param>
/// <param name="cancellationToken">cancellation Token.</param>
/// <returns>bound activity ready to send to the user.</returns>
protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, string conversationType = default, IList<string> toList = default, CancellationToken cancellationToken = default(CancellationToken))
{
// Get base prompt text (if any)
var text = prompt != null && !string.IsNullOrEmpty(prompt.Text) ? prompt.Text : string.Empty;
Expand All @@ -396,7 +413,7 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
break;

case ListStyle.SuggestedAction:
msg = ChoiceFactory.SuggestedAction(choices, text);
msg = ChoiceFactory.SuggestedAction(choices, text, null, toList);
break;

case ListStyle.HeroCard:
Expand All @@ -409,7 +426,7 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
break;

default:
msg = ChoiceFactory.ForChannel(channelId, choices, text, null, options);
msg = ChoiceFactory.ForChannel(channelId, choices, text, null, options, conversationType, toList);
break;
}

Expand Down
21 changes: 21 additions & 0 deletions libraries/Microsoft.Bot.Builder.Dialogs/Choices/Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public class Channel
/// <param name="buttonCnt">(Optional) The number of Suggested Actions to check for the Channel.</param>
/// <returns>True if the Channel supports the buttonCnt total Suggested Actions, False if the Channel does not support that number of Suggested Actions.</returns>
public static bool SupportsSuggestedActions(string channelId, int buttonCnt = 100)
{
return SupportsSuggestedActions(channelId, buttonCnt, null);
}

/// <summary>
/// Determine if a number of Suggested Actions are supported by a Channel.
/// </summary>
/// <param name="channelId">The Channel to check the if Suggested Actions are supported in.</param>
/// <param name="buttonCnt">(Optional) The number of Suggested Actions to check for the Channel.</param>
/// <param name="conversationType">(Optional) The type of the conversation.</param>
/// <returns>True if the Channel supports the buttonCnt total Suggested Actions, False if the Channel does not support that number of Suggested Actions.</returns>
public static bool SupportsSuggestedActions(string channelId, int buttonCnt = 100, string conversationType = default)
{
switch (channelId)
{
Expand All @@ -42,6 +54,15 @@ public static bool SupportsSuggestedActions(string channelId, int buttonCnt = 10
case Connector.Channels.Webchat:
return buttonCnt <= 100;

// https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/conversation-messages?tabs=dotnet1%2Cdotnet2%2Cdotnet3%2Cdotnet4%2Cdotnet5%2Cdotnet#send-suggested-actions
case Connector.Channels.Msteams:
if (conversationType == "personal")
{
return buttonCnt <= 3;
}

return false;

default:
return false;
}
Expand Down
56 changes: 52 additions & 4 deletions libraries/Microsoft.Bot.Builder.Dialogs/Choices/ChoiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ public class ChoiceFactory
/// <para>If the algorithm decides to use a list, for 3 or fewer choices with short titles it will use an inline
/// list; otherwise, a numbered list.</para></remarks>
public static IMessageActivity ForChannel(string channelId, IList<Choice> list, string text = null, string speak = null, ChoiceFactoryOptions options = null)
{
return ForChannel(channelId, list, text, speak, options, null, null);
}

/// <summary>
/// Creates a message activity that includes a list of choices formatted based on the capabilities of a given channel.
/// </summary>
/// <param name="channelId">A channel ID. The <see cref="Connector.Channels"/> class contains known channel IDs.</param>
/// <param name="list">The list of choices to include.</param>
/// <param name="text">Optional, the text of the message to send.</param>
/// <param name="speak">Optional, the text to be spoken by your bot on a speech-enabled channel.</param>
/// <param name="options">Optional, the formatting options to use when rendering as a list.</param>
/// <param name="conversationType">Optional, the type of the conversation.</param>
/// <param name="toList">Optional, the list of recipients.</param>
/// <returns>The created message activity.</returns>
/// <remarks>The algorithm prefers to format the supplied list of choices as suggested actions but can decide
/// to use a text based list if suggested actions aren't natively supported by the channel, there are too many
/// choices for the channel to display, or the title of any choice is too long.
/// <para>If the algorithm decides to use a list, for 3 or fewer choices with short titles it will use an inline
/// list; otherwise, a numbered list.</para></remarks>
public static IMessageActivity ForChannel(string channelId, IList<Choice> list, string text = null, string speak = null, ChoiceFactoryOptions options = null, string conversationType = default, IList<string> toList = default)
{
channelId ??= string.Empty;
list ??= new List<Choice>();
Expand All @@ -46,7 +67,7 @@ public static IMessageActivity ForChannel(string channelId, IList<Choice> list,
}

// Determine list style
var supportsSuggestedActions = Channel.SupportsSuggestedActions(channelId, list.Count);
var supportsSuggestedActions = Channel.SupportsSuggestedActions(channelId, list.Count, conversationType);
var supportsCardActions = Channel.SupportsCardActions(channelId, list.Count);
var maxActionTitleLength = Channel.MaxActionTitleLength(channelId);
var hasMessageFeed = Channel.HasMessageFeed(channelId);
Expand All @@ -63,7 +84,7 @@ public static IMessageActivity ForChannel(string channelId, IList<Choice> list,
{
// We always prefer showing choices using suggested actions. If the titles are too long, however,
// we'll have to show them as a text list.
return SuggestedAction(list, text, speak);
return SuggestedAction(list, text, speak, toList);
}

if (!longTitles && list.Count <= 3)
Expand Down Expand Up @@ -198,7 +219,20 @@ public static Activity List(IList<Choice> choices, string text = null, string sp
/// <returns>An activity with choices as suggested actions.</returns>
public static IMessageActivity SuggestedAction(IList<string> choices, string text = null, string speak = null)
{
return SuggestedAction(ToChoices(choices), text, speak);
return SuggestedAction(ToChoices(choices), text, speak, null);
}

/// <summary>
/// Creates a message activity containing a list of choices that have been added as suggested actions.
/// </summary>
/// <param name="choices">List of strings representing the choices to add.</param>
/// <param name="text">Optional, text of the message.</param>
/// <param name="speak">Optional, SSML text to be spoken by the bot on a speech-enabled channel.</param>
/// <param name="toList">Optional, the list of recipients.</param>
/// <returns>An activity with choices as suggested actions.</returns>
public static IMessageActivity SuggestedAction(IList<string> choices, string text = null, string speak = null, IList<string> toList = default)
{
return SuggestedAction(ToChoices(choices), text, speak, toList);
}

/// <summary>
Expand All @@ -211,7 +245,21 @@ public static IMessageActivity SuggestedAction(IList<string> choices, string tex
public static IMessageActivity SuggestedAction(IList<Choice> choices, string text = null, string speak = null)
{
// Return activity with choices as suggested actions
return MessageFactory.SuggestedActions(ExtractActions(choices), text, speak, InputHints.ExpectingInput);
return SuggestedAction(choices, text, speak, null);
}

/// <summary>
/// Creates a message activity containing a list of choices that have been added as suggested actions.
/// </summary>
/// <param name="choices">The list of choices to add.</param>
/// <param name="text">Optional, text of the message.</param>
/// <param name="speak">Optional, SSML text to be spoken by the bot on a speech-enabled channel.</param>
/// <param name="toList">Optional, the list of recipients.</param>
/// <returns>An activity with choices as suggested actions.</returns>
public static IMessageActivity SuggestedAction(IList<Choice> choices, string text = null, string speak = null, IList<string> toList = default)
{
// Return activity with choices as suggested actions
return MessageFactory.SuggestedActions(ExtractActions(choices), text, speak, InputHints.ExpectingInput, toList);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,16 @@ protected override async Task OnPromptAsync(
var channelId = turnContext.Activity.ChannelId;
var choiceOptions = ChoiceOptions ?? _choiceDefaults[culture];
var choiceStyle = options.Style ?? Style;
var conversationType = turnContext.Activity.Conversation?.ConversationType;
var recipient = turnContext.Activity.From?.Id;
var toList = string.IsNullOrEmpty(recipient) ? null : new List<string>() { recipient };
if (isRetry && options.RetryPrompt != null)
{
prompt = AppendChoices(options.RetryPrompt, channelId, choices, choiceStyle, choiceOptions);
prompt = AppendChoices(options.RetryPrompt, channelId, choices, choiceStyle, choiceOptions, conversationType, toList);
}
else
{
prompt = AppendChoices(options.Prompt, channelId, choices, choiceStyle, choiceOptions);
prompt = AppendChoices(options.Prompt, channelId, choices, choiceStyle, choiceOptions, conversationType, toList);
}

// Send prompt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,16 @@ protected override async Task OnPromptAsync(
var confirmChoices = ConfirmChoices ?? Tuple.Create(defaults.Item1, defaults.Item2);
var choices = new List<Choice> { confirmChoices.Item1, confirmChoices.Item2 };
var style = options.Style ?? Style;
var conversationType = turnContext.Activity.Conversation?.ConversationType;
var recipient = turnContext.Activity.From?.Id;
var toList = string.IsNullOrEmpty(recipient) ? null : new List<string>() { recipient };
if (isRetry && options.RetryPrompt != null)
{
prompt = AppendChoices(options.RetryPrompt, channelId, choices, style, choiceOptions);
prompt = AppendChoices(options.RetryPrompt, channelId, choices, style, choiceOptions, conversationType, toList);
}
else
{
prompt = AppendChoices(options.Prompt, channelId, choices, style, choiceOptions);
prompt = AppendChoices(options.Prompt, channelId, choices, style, choiceOptions, conversationType, toList);
}

// Send prompt
Expand Down
23 changes: 21 additions & 2 deletions libraries/Microsoft.Bot.Builder.Dialogs/Prompts/Prompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,25 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <remarks>If the task is successful, the result contains the updated activity.</remarks>
protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, CancellationToken cancellationToken = default)
{
return AppendChoices(prompt, channelId, choices, style, options, null, null, cancellationToken);
}

/// <summary>
/// When overridden in a derived class, appends choices to the activity when the user is prompted for input.
/// </summary>
/// <param name="prompt">The activity to append the choices to.</param>
/// <param name="channelId">The ID of the user's channel.</param>
/// <param name="choices">The choices to append.</param>
/// <param name="style">Indicates how the choices should be presented to the user.</param>
/// <param name="options">The formatting options to use when presenting the choices.</param>
/// <param name="conversationType">The type of the conversation.</param>
/// <param name="toList">The list of recipients.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <remarks>If the task is successful, the result contains the updated activity.</remarks>
protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, string conversationType = default, IList<string> toList = default, CancellationToken cancellationToken = default)
{
// Get base prompt text (if any)
var text = prompt != null && !string.IsNullOrEmpty(prompt.Text) ? prompt.Text : string.Empty;
Expand All @@ -293,7 +312,7 @@ protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string
break;

case ListStyle.SuggestedAction:
msg = ChoiceFactory.SuggestedAction(choices, text);
msg = ChoiceFactory.SuggestedAction(choices, text, null, toList);
break;

case ListStyle.HeroCard:
Expand All @@ -306,7 +325,7 @@ protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string
break;

default:
msg = ChoiceFactory.ForChannel(channelId, choices, text, null, options);
msg = ChoiceFactory.ForChannel(channelId, choices, text, null, options, conversationType, toList);
break;
}

Expand Down
Loading

0 comments on commit 5549586

Please sign in to comment.