Skip to content

Commit

Permalink
Release/070220 (#196)
Browse files Browse the repository at this point in the history
**New features**

* Added AdaptiveCardPrompt (#193) - Thanks to Michael Richardson <[email protected]>

**Bug fixes**

* Update Google Content Type

**Other updates**

* Fixed some NuGet settings in project files
  • Loading branch information
garypretty authored Feb 7, 2020
1 parent 2b39c40 commit a3838eb
Show file tree
Hide file tree
Showing 18 changed files with 1,588 additions and 33 deletions.
56 changes: 29 additions & 27 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<Version>1.0.0</Version>
<FileVersion>1.0.0</FileVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<RepositoryUrl>http://www.github.com/botbuildercommunity/botbuildercommunity-dotnet
</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<Version>1.0.0</Version>
<FileVersion>1.0.0</FileVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<RepositoryUrl>http://www.github.com/botbuildercommunity/botbuildercommunity-dotnet
</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public GoogleHttpAdapter(IHttpContextAccessor httpContextAccessor)
throw new ArgumentNullException(nameof(googleResponse));
}

httpResponse.ContentType = "application/json";
httpResponse.ContentType = "application/json;charset=utf-8";
httpResponse.StatusCode = (int)HttpStatusCode.OK;

using (var writer = new StreamWriter(httpResponse.Body))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public async Task<HttpResponseMessage> ProcessMessageRequestAsync(HttpRequestMes

var response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(GoogleResponseBodyJson);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json;charset=utf-8");

return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<Authors>Tomislav Markovski</Authors>
<Company>Bot Builder Community</Company>
<Product />
<RepositoryUrl>http://www.github.com/botbuildercommunity/botbuildercommunity-dotnet
</RepositoryUrl>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<Version>1.0.0</Version>
<FileVersion>1.0.0</FileVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<Authors>Bot Builder Community</Authors>
<Company>Bot Builder Community</Company>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageLicenseUrl>https://github.com/botbuildercommunity/botbuilder-community-dotnet/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/botbuildercommunity/botbuilder-community-dotnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/botbuildercommunity/botbuilder-community-dotnet</RepositoryUrl>
<PackageTags>bot framework, bot builder, azure bot service, location dialog, dialogs</PackageTags>
<PackageTags>bot framework;bot builder;azure bot service;location dialog;dialogs</PackageTags>
<Version>1.0.0</Version>
<FileVersion>1.0.0</FileVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<Copyright>Microsoft Corporation. All rights reserved.</Copyright>
<PackageLicenseUrl>https://github.com/botbuildercommunity/botbuilder-community-dotnet/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/botbuildercommunity/botbuilder-community-dotnet</PackageProjectUrl>
<PackageTags>bot framework, bot builder, azure bot service, luis dialog, dialogs, luis</PackageTags>
<PackageTags>bot framework;bot builder;azure bot service;luis dialog;dialogs;luis</PackageTags>
<RepositoryUrl>https://github.com/botbuildercommunity/botbuilder-community-dotnet</RepositoryUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
Expand Down
256 changes: 256 additions & 0 deletions libraries/Bot.Builder.Community.Dialogs.Prompts/AdaptiveCardPrompt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Schema;
using Newtonsoft.Json.Linq;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;

namespace Bot.Builder.Community.Dialogs.Prompts
{
public enum AdaptiveCardPromptErrors
{
/// <summary>
/// No known user errors.
/// </summary>
None,

/// <summary>
/// Error presented if developer specifies AdaptiveCardPromptSettings.promptId,
/// but user submits adaptive card input on a card where the ID does not match.
/// This error will also be present if developer AdaptiveCardPromptSettings.promptId,
/// but forgets to add the promptId to every <submit>.data.promptId in your Adaptive Card.
/// </summary>
UserInputDoesNotMatchCardId,

/// <summary>
/// Error presented if developer specifies AdaptiveCardPromptSettings.requiredIds,
/// but user does not submit input for all required input id's on the adaptive card.
/// </summary>
MissingRequiredIds,

/// <summary>
/// Error presented if user enters plain text instead of using Adaptive Card's input fields.
/// </summary>
UserUsedTextInput,
}

/// <summary>
/// Waits for Adaptive Card Input to be received.
/// </summary>
/// <remarks>
/// This prompt is similar to ActivityPrompt but provides features specific to Adaptive Cards:
/// * Optionally allow specified input fields to be required
/// * Optionally ensures input is only valid if it comes from the appropriate card (not one shown previous to prompt)
/// * Provides ability to handle variety of common user errors related to Adaptive Cards
/// DO NOT USE WITH CHANNELS THAT DON'T SUPPORT ADAPTIVE CARDS.
/// </remarks>
public class AdaptiveCardPrompt : Dialog
{
private const string PersistedOptions = "options";
private const string PersistedState = "state";
public const string AttemptCountKey = "AttemptCount";

// Has to be dynamic because PromptValidator is internal to the SDK
private readonly AdaptiveCardPromptValidator<AdaptiveCardPromptResult> _validator;
private readonly string[] _requiredInputIds;
private readonly string _promptId;
private readonly Attachment _card;

/// <summary>
/// Initializes a new instance of the <see cref="AdaptiveCardPrompt"/> class.
/// </summary>
/// <param name="dialogId">Unique ID of the dialog within its parent `DialogSet` or `ComponentDialog`.</param>
/// <param name="validator">(optional) Validator that will be called each time a new activity is received.</param>
/// <param name="settings">(optional) Additional settings for AdaptiveCardPrompt behavior.</param>
public AdaptiveCardPrompt(string dialogId, AdaptiveCardPromptSettings settings, AdaptiveCardPromptValidator<AdaptiveCardPromptResult> validator = null)
: base(dialogId)
{
if (settings == null || settings.Card == null)
{
throw new ArgumentNullException("AdaptiveCardPrompt requires a card in `AdaptiveCardPromptSettings.card`");
}

_validator = validator;

_requiredInputIds = settings.RequiredInputIds ?? null;

ThrowIfNotAdaptiveCard(settings.Card);
_card = settings.Card;

_promptId = settings.PromptId;
}

public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken))
{
// Initialize prompt state
var state = dc.ActiveDialog.State;
state[PersistedOptions] = options;
state[PersistedState] = new Dictionary<string, object>
{
{ AttemptCountKey, 0 },
};

// Send initial prompt
await OnPromptAsync(dc.Context, (IDictionary<string, object>)state[PersistedState], (PromptOptions)state[PersistedOptions], false, cancellationToken).ConfigureAwait(false);

return Dialog.EndOfTurn;
}

// Override ContinueDialogAsync so that we can catch Activity.Value (which is ignored, by default)
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken)
{
// Perform base recognition
var instance = dc.ActiveDialog;
var state = (IDictionary<string, object>)instance.State[PersistedState];
var options = (PromptOptions)instance.State[PersistedOptions];
var recognized = await OnRecognizeAsync(dc.Context, cancellationToken).ConfigureAwait(false);

// Increment attempt count
// Convert.ToInt32 For issue https://github.com/Microsoft/botbuilder-dotnet/issues/1859
state[AttemptCountKey] = Convert.ToInt32(state[AttemptCountKey]) + 1;

var isValid = false;
if (_validator != null)
{
var promptContext = new AdaptiveCardPromptValidatorContext<AdaptiveCardPromptResult>(dc.Context, recognized, state, options);
isValid = await _validator(promptContext, cancellationToken).ConfigureAwait(false);
}
else if (recognized.Succeeded)
{
isValid = true;
}

// Return recognized value or re-prompt
if (isValid)
{
return await dc.EndDialogAsync(recognized.Value).ConfigureAwait(false);
}
else
{
// Re-prompt
if (!dc.Context.Responded)
{
await OnPromptAsync(dc.Context, state, options, true, cancellationToken).ConfigureAwait(false);
}

return Dialog.EndOfTurn;
}
}

protected virtual async Task OnPromptAsync(ITurnContext context, IDictionary<string, object> state, PromptOptions options, bool isRetry, CancellationToken cancellationToken)
{
// Since card is passed in via AdaptiveCardPromptSettings, PromptOptions may not be used.
// Ensure we're working with RetryPrompt, as applicable
var prompt = isRetry && options.RetryPrompt != null ? options.RetryPrompt : options.Prompt;

// Clone the correct prompt (or new Activity, if null) so that we don't affect the one saved in state
var clonedPrompt = JObject.FromObject(prompt ?? new Activity()).ToObject<Activity>();

// The actual AdaptiveCard should be tightly-coupled to its AdaptiveCardPromptSettings, which means it would be instantiated in
// a Dialog's constructor and passed in when using AddDialog(AdaptiveCardPrompt) as opposed to passing into Activity.Attachments.
// The actual prompt is typically called by using stepContext.PromptAsync() in a WaterfallDialog step, where PromptOptions is
// required by DialogContext. However, PromptOptions isn't really required (or useful) for AdaptiveCardPrompt.
// This next code block allows a user to simply call by setting Activity.Type to ActivityTypes.Message
// "PromptAsync(nameof(AdaptiveCardPrompt), new PromptOptions())", instead of the uglier
// "PromptAsync(nameof(AdaptiveCardPrompt), new PromptOptions(){ Prompt = MessageFactory.Text(string.Empty) })"
// Note: PromptOptions does not need to be set in Node, since it compiles to JavaScript
// and the card gets sent without setting Activity.Type
if (clonedPrompt.Type == null)
{
clonedPrompt.Type = ActivityTypes.Message;
}

// Add Adaptive Card as last attachment (user input should go last), keeping any others
if (clonedPrompt.Attachments == null)
{
clonedPrompt.Attachments = new List<Attachment>();
}

clonedPrompt.Attachments.Add(_card);

await context.SendActivityAsync(clonedPrompt, cancellationToken).ConfigureAwait(false);
}

protected virtual Task<PromptRecognizerResult<AdaptiveCardPromptResult>> OnRecognizeAsync(ITurnContext context, CancellationToken cancellationToken)
{
// Ignore user input that doesn't come from adaptive card
if (string.IsNullOrWhiteSpace(context.Activity.Text) && context.Activity.Value != null)
{
var data = JObject.FromObject(context.Activity.Value);

// Validate it comes from the correct card - This is only a worry while the prompt/dialog has not ended
if (!string.IsNullOrEmpty(_promptId) && data["promptId"]?.ToString() != _promptId)
{
return Task.FromResult(new PromptRecognizerResult<AdaptiveCardPromptResult>()
{
Succeeded = false,
Value = new AdaptiveCardPromptResult()
{
Data = data,
Error = AdaptiveCardPromptErrors.UserInputDoesNotMatchCardId,
},
});
}

// Check for required input data, if specified in AdaptiveCardPromptSettings
var missingIds = new List<string>();
foreach (var id in _requiredInputIds ?? Enumerable.Empty<string>())
{
if (data[id] == null || string.IsNullOrWhiteSpace(data[id].ToString()))
{
missingIds.Add(id);
}
}

// User did not submit inputs that were required
if (missingIds.Count > 0)
{
return Task.FromResult(new PromptRecognizerResult<AdaptiveCardPromptResult>()
{
Succeeded = false,
Value = new AdaptiveCardPromptResult()
{
Data = data,
Error = AdaptiveCardPromptErrors.MissingRequiredIds,
MissingIds = missingIds,
},
});
}

return Task.FromResult(new PromptRecognizerResult<AdaptiveCardPromptResult>()
{
Succeeded = true,
Value = new AdaptiveCardPromptResult() { Data = data },
});
}
else
{
// User used text input instead of card input
return Task.FromResult(new PromptRecognizerResult<AdaptiveCardPromptResult>()
{
Succeeded = false,
Value = new AdaptiveCardPromptResult() { Error = AdaptiveCardPromptErrors.UserUsedTextInput },
});
}
}

private void ThrowIfNotAdaptiveCard(Attachment cardAttachment)
{
var adaptiveCardType = "application/vnd.microsoft.card.adaptive";

if (cardAttachment == null || cardAttachment.Content == null)
{
throw new NullReferenceException($"No Adaptive Card provided. Include in the constructor or PromptOptions.Prompt.Attachments");
}
else if (string.IsNullOrEmpty(cardAttachment.ContentType) || cardAttachment.ContentType != adaptiveCardType)
{
throw new ArgumentException($"Attachment is not a valid Adaptive Card.\n" +
$"Ensure Card.ContentType is '${adaptiveCardType}'\n" +
"and Card.Content contains the card json");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections.Generic;

namespace Bot.Builder.Community.Dialogs.Prompts
{
/// <summary>
/// Represents a result from adaptive card input.
/// </summary>
/// <typeparam name="T">Type returned by recognizer.</typeparam>
public class AdaptiveCardPromptResult
{
/// <summary>
/// Gets or sets the Value of the Adaptive Card input.
/// </summary>
/// <value>
/// The value of the user's input from the Adaptive Card.
/// </value>
public object Data { get; set; }

/// <summary>
/// Gets or sets Error enum.
/// </summary>
/// <value>
/// If not recognized.succeeded, include reason why, if known.
/// </value>
public AdaptiveCardPromptErrors Error { get; set; }

/// <summary>
/// Gets or sets array of missing required Ids.
/// </summary>
/// <value>
/// Array of requiredIds that were not included with user input.
/// </value>
public List<string> MissingIds { get; set; }
}
}
Loading

0 comments on commit a3838eb

Please sign in to comment.