diff --git a/Microsoft.Bot.Builder.sln b/Microsoft.Bot.Builder.sln index 23f13c756..b1d8e739b 100644 --- a/Microsoft.Bot.Builder.sln +++ b/Microsoft.Bot.Builder.sln @@ -40,6 +40,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Samples.Ai.QnA", "samples\Microsoft.Bot.Samples.Ai.QnA\Microsoft.Bot.Samples.Ai.QnA.csproj", "{A8CE6B0F-E054-45F7-B4D7-23D2C65D2D26}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Builder.Prompts", "libraries\Microsoft.Bot.Builder.Prompts\Microsoft.Bot.Builder.Prompts.csproj", "{7A197F41-3411-47BF-87A6-5BC8A44CDB74}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Builder.Prompts.Tests", "tests\Microsoft.Bot.Builder.Prompts.Tests\Microsoft.Bot.Builder.Prompts.Tests.csproj", "{5D145BFC-1C30-4FC5-989A-82260F8225BE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +114,14 @@ Global {A8CE6B0F-E054-45F7-B4D7-23D2C65D2D26}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8CE6B0F-E054-45F7-B4D7-23D2C65D2D26}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8CE6B0F-E054-45F7-B4D7-23D2C65D2D26}.Release|Any CPU.Build.0 = Release|Any CPU + {7A197F41-3411-47BF-87A6-5BC8A44CDB74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A197F41-3411-47BF-87A6-5BC8A44CDB74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A197F41-3411-47BF-87A6-5BC8A44CDB74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A197F41-3411-47BF-87A6-5BC8A44CDB74}.Release|Any CPU.Build.0 = Release|Any CPU + {5D145BFC-1C30-4FC5-989A-82260F8225BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D145BFC-1C30-4FC5-989A-82260F8225BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D145BFC-1C30-4FC5-989A-82260F8225BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D145BFC-1C30-4FC5-989A-82260F8225BE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -131,6 +143,8 @@ Global {C8DF8AE3-0A4D-445D-993E-F7A1784BFDD4} = {3ADFB27A-95FA-4330-B211-1D66A29A17AB} {BF414C86-DB3B-4022-9B29-DCE8AA954C12} = {AD743B78-D61F-4FBF-B620-FA83CE599A50} {A8CE6B0F-E054-45F7-B4D7-23D2C65D2D26} = {3ADFB27A-95FA-4330-B211-1D66A29A17AB} + {7A197F41-3411-47BF-87A6-5BC8A44CDB74} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A} + {5D145BFC-1C30-4FC5-989A-82260F8225BE} = {AD743B78-D61F-4FBF-B620-FA83CE599A50} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16} diff --git a/libraries/Microsoft.Bot.Builder.Core/Adapters/TestAdapter.cs b/libraries/Microsoft.Bot.Builder.Core/Adapters/TestAdapter.cs index e48c0e31d..c8005819e 100644 --- a/libraries/Microsoft.Bot.Builder.Core/Adapters/TestAdapter.cs +++ b/libraries/Microsoft.Bot.Builder.Core/Adapters/TestAdapter.cs @@ -12,7 +12,7 @@ namespace Microsoft.Bot.Builder.Adapters public class TestAdapter : BotAdapter { private int _nextId = 0; - private readonly List botReplies = new List(); + private readonly List botReplies = new List(); public TestAdapter(ConversationReference reference = null) { @@ -290,7 +290,12 @@ namespace Microsoft.Bot.Builder.Adapters if (expected.Type != reply.Type) throw new Exception($"{description}: Type should match"); if (expected.AsMessageActivity().Text != reply.AsMessageActivity().Text) - throw new Exception($"{description}: Text should match"); + { + if (description == null) + throw new Exception($"Expected:{expected.AsMessageActivity().Text}\nReceived:{reply.AsMessageActivity().Text}"); + else + throw new Exception($"{description}: Text should match"); + } // TODO, expand this to do all properties set on expected }, description, timeout); } diff --git a/libraries/Microsoft.Bot.Builder.Prompts/AgePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/AgePrompt.cs new file mode 100644 index 000000000..722ca60e9 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/AgePrompt.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + + /// + /// AgePrompt recognizes age expressions like "95 years" + /// + public class AgePrompt : NumberWithUnitPrompt + { + public AgePrompt(string culture, PromptValidator validator = null) + : base(NumberWithUnitRecognizer.Instance.GetAgeModel(culture), validator) + { + } + + protected AgePrompt(IModel model, PromptValidator validator = null) + : base(model, validator) + { + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/BasePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/BasePrompt.cs new file mode 100644 index 000000000..a7b2c8218 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/BasePrompt.cs @@ -0,0 +1,57 @@ +using Microsoft.Bot.Schema; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + public abstract class BasePrompt + { + private readonly PromptValidator _customValidator = null; + + public BasePrompt(PromptValidator validator = null) + { + _customValidator = validator; + } + + /// + /// Creates a new Message, and queues it for sending to the user. + /// + public Task Prompt(IBotContext context, string text, string speak = null) + { + IMessageActivity activity = MessageFactory.Text(text, speak); + activity.InputHint = InputHints.ExpectingInput; + return Prompt(context, activity); + } + + /// + /// Creates a new Message Activity, and queues it for sending to the user. + /// + public Task Prompt(IBotContext context, IMessageActivity activity) + { + context.Reply(activity); + return Task.CompletedTask; + } + + /// + /// implement to recognize the basic type + /// + /// + /// null if not recognized + public abstract Task Recognize(IBotContext context); + + + protected virtual Task Validate(IBotContext context, T value) + { + // Validation passed. Return the validated text. + if (_customValidator != null) + { + return _customValidator(context, value); + } + return Task.FromResult(true); + } + + } +} \ No newline at end of file diff --git a/libraries/Microsoft.Bot.Builder.Prompts/CurrencyPrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/CurrencyPrompt.cs new file mode 100644 index 000000000..0d379648d --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/CurrencyPrompt.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + + /// + /// CurrencyPrompt recognizes currency expressions as float type + /// + public class CurrencyPrompt : NumberWithUnitPrompt + { + public CurrencyPrompt(string culture, PromptValidator validator = null) + : base(NumberWithUnitRecognizer.Instance.GetCurrencyModel(culture), validator) + { + } + + protected CurrencyPrompt(IModel model, PromptValidator validator = null) + : base(model, validator) + { + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/DateTimePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/DateTimePrompt.cs new file mode 100644 index 000000000..36efb777c --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/DateTimePrompt.cs @@ -0,0 +1,82 @@ +//using System; +//using System.Linq; +//using System.Threading.Tasks; +//using Microsoft.Bot.Schema; +//using Microsoft.Recognizers.Text; +//using Microsoft.Recognizers.Text.Number; +//using Microsoft.Recognizers.Text.NumberWithUnit; +//using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +//namespace Microsoft.Bot.Builder.Prompts +//{ +// public class NumberWithUnit +// { +// public NumberWithUnit() { } + +// public string Unit { get; set; } + +// public float Amount { get; set; } + +// public string Text { get; set; } +// } + +// /// +// /// CurrencyPrompt recognizes currency expressions as float type +// /// +// public class NumberWithUnitPrompt : BasePrompt +// { +// private readonly PromptValidator _customValidator = null; +// private IModel _model; + + +// protected NumberWithUnitPrompt(IModel model, PromptValidator validator = null) +// { +// _model = model ?? throw new ArgumentNullException(nameof(model)); +// _customValidator = validator; +// } + +// /// +// /// Used to validate the incoming text, expected on context.Request, is +// /// valid according to the rules defined in the validation steps. +// /// +// public override async Task Recognize(IBotContext context) +// { +// BotAssert.ContextNotNull(context); +// BotAssert.ActivityNotNull(context.Request); +// if (context.Request.Type != ActivityTypes.Message) +// throw new InvalidOperationException("No Message to Recognize"); + +// IMessageActivity message = context.Request.AsMessageActivity(); +// var results = _model.Parse(message.Text); +// if (results.Any()) +// { +// var result = results.First(); +// NumberWithUnit value = new NumberWithUnit() +// { +// Text = result.Text, +// Unit = (string)result.Resolution["unit"], +// Amount = float.NaN +// }; +// if (float.TryParse(result.Resolution["amount"]?.ToString() ?? String.Empty, out float val)) +// value.Amount = val; + +// if (await Validate(context, value)) +// return value; +// } +// return null; +// } + + +// protected Task Validate(IBotContext context, NumberWithUnit value) +// { +// // Validation passed. Return the validated text. +// if (_customValidator != null) +// { +// return _customValidator(context, value); +// } +// return Task.FromResult(true); +// } + +// } +//} +//} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/DimensionPrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/DimensionPrompt.cs new file mode 100644 index 000000000..1c5cf216d --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/DimensionPrompt.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + + /// + /// DimensionPrompt recognizes dimension expressions like "4 feet" or "6 miles" + /// + public class DimensionPrompt : NumberWithUnitPrompt + { + public DimensionPrompt(string culture, PromptValidator validator = null) + : base(NumberWithUnitRecognizer.Instance.GetDimensionModel(culture), validator) + { + } + + protected DimensionPrompt(IModel model, PromptValidator validator = null) + : base(model, validator) + { + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/Microsoft.Bot.Builder.Prompts.csproj b/libraries/Microsoft.Bot.Builder.Prompts/Microsoft.Bot.Builder.Prompts.csproj new file mode 100644 index 000000000..2b4f24f28 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/Microsoft.Bot.Builder.Prompts.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + diff --git a/libraries/Microsoft.Bot.Builder.Prompts/NumberPrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/NumberPrompt.cs new file mode 100644 index 000000000..29475c755 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/NumberPrompt.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + public class NumberResult + { + public T Value { get; set; } + + public string Text { get; set; } + } + + /// + /// NumberPrompt recognizes floats or ints + /// + public class NumberPrompt : BasePrompt> + { + private IModel _model; + + public NumberPrompt(string culture, PromptValidator> validator = null) + : base(validator) + { + _model = NumberRecognizer.Instance.GetNumberModel(culture); + } + + protected NumberPrompt(IModel model, PromptValidator> validator = null) + : base(validator) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + } + + /// + /// Used to validate the incoming text, expected on context.Request, is + /// valid according to the rules defined in the validation steps. + /// + public override async Task> Recognize(IBotContext context) + { + BotAssert.ContextNotNull(context); + BotAssert.ActivityNotNull(context.Request); + if (context.Request.Type != ActivityTypes.Message) + throw new InvalidOperationException("No Message to Recognize"); + + IMessageActivity message = context.Request.AsMessageActivity(); + var results = _model.Parse(message.Text); + if (results.Any()) + { + var result = results.First(); + if (typeof(T) == typeof(float)) + { + if (float.TryParse(result.Resolution["value"].ToString(), out float value)) + { + NumberResult numberResult = new NumberResult() + { + Value = (T)(object)value, + Text = result.Text + }; + if (await Validate(context, numberResult)) + return numberResult; + } + } + else if (typeof(T) == typeof(int)) + { + if (int.TryParse(result.Resolution["value"].ToString(), out int value)) + { + NumberResult numberResult = new NumberResult() + { + Value = (T)(object)value, + Text = result.Text + }; + if (await Validate(context, numberResult)) + return numberResult; + } + } + } + return null; + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/NumberWithUnitPrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/NumberWithUnitPrompt.cs new file mode 100644 index 000000000..aca198579 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/NumberWithUnitPrompt.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + public class NumberWithUnit + { + public NumberWithUnit() { } + + public string Unit { get; set; } + + public float Value { get; set; } + + public string Text { get; set; } + } + + /// + /// CurrencyPrompt recognizes currency expressions as float type + /// + public class NumberWithUnitPrompt : BasePrompt + { + private IModel _model; + + + protected NumberWithUnitPrompt(IModel model, PromptValidator validator = null) + :base(validator) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + } + + /// + /// Used to validate the incoming text, expected on context.Request, is + /// valid according to the rules defined in the validation steps. + /// + public override async Task Recognize(IBotContext context) + { + BotAssert.ContextNotNull(context); + BotAssert.ActivityNotNull(context.Request); + if (context.Request.Type != ActivityTypes.Message) + throw new InvalidOperationException("No Message to Recognize"); + + IMessageActivity message = context.Request.AsMessageActivity(); + var results = _model.Parse(message.Text); + if (results.Any()) + { + var result = results.First(); + NumberWithUnit value = new NumberWithUnit() + { + Text = result.Text, + Unit = (string)result.Resolution["unit"], + Value = float.NaN + }; + if (float.TryParse(result.Resolution["value"]?.ToString() ?? String.Empty, out float val)) + value.Value = val; + + if (await Validate(context, value)) + return value; + } + return null; + } + + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/OrdinalPrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/OrdinalPrompt.cs new file mode 100644 index 000000000..e56cf2090 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/OrdinalPrompt.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + + /// + /// OrdinalPrompt recognizes pharses like First, 2nd, third, etc. + /// + public class OrdinalPrompt : NumberPrompt + { + + public OrdinalPrompt(string culture, PromptValidator> validator = null) + : base(NumberRecognizer.Instance.GetOrdinalModel(culture), validator) + { + } + + protected OrdinalPrompt(IModel model, PromptValidator> validator = null) + : base(model, validator) + { + } + + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/PercentagePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/PercentagePrompt.cs new file mode 100644 index 000000000..71e7f6276 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/PercentagePrompt.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + + /// + /// PercentagePrompt recognizes percentage expressions as float type + /// + public class PercentagePrompt : BasePrompt> + { + private IModel _model; + + public PercentagePrompt(string culture, PromptValidator> validator = null) + : base( validator) + { + _model = NumberRecognizer.Instance.GetPercentageModel(culture); + } + + + protected PercentagePrompt(IModel model, PromptValidator> validator = null) + : base(validator) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + } + + /// + /// Used to validate the incoming text, expected on context.Request, is + /// valid according to the rules defined in the validation steps. + /// + public override async Task> Recognize(IBotContext context) + { + BotAssert.ContextNotNull(context); + BotAssert.ActivityNotNull(context.Request); + if (context.Request.Type != ActivityTypes.Message) + throw new InvalidOperationException("No Message to Recognize"); + + IMessageActivity message = context.Request.AsMessageActivity(); + var results = _model.Parse(message.Text); + if (results.Any()) + { + var result = results.First(); + if (float.TryParse(result.Resolution["value"].ToString().TrimEnd('%'), out float value)) + { + NumberResult numberResult = new NumberResult() + { + Value = value, + Text = result.Text + }; + if (await Validate(context, numberResult)) + return numberResult; + } + } + return null; + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/PhonePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/PhonePrompt.cs new file mode 100644 index 000000000..506167fef --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/PhonePrompt.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using Microsoft.Recognizers.Text.Sequence; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + /// + /// PhoneNumberPrompt recognizes phone numbers + /// + public class PhoneNumberPrompt : ValuePrompt + { + public PhoneNumberPrompt(string culture, PromptValidator validator = null) : + base(SequenceRecognizer.Instance.GetPhoneNumberModel(culture), validator) + { + } + + protected PhoneNumberPrompt(IModel model, PromptValidator validator = null) : + base(model, validator) + { + } + + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/PromptValidator.cs b/libraries/Microsoft.Bot.Builder.Prompts/PromptValidator.cs new file mode 100644 index 000000000..2f6e6e338 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/PromptValidator.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Bot.Builder.Prompts +{ + public static class PromptValidatorEx + { + /// + /// Signature of a handler that can be passed to a prompt to provide additional validation logic + /// or to customize the reply sent to the user when their response is invalid. + /// + /// Type of value that will recognized and passed to the validator as input + /// Context for the current turn of conversation. + /// + /// true or false task + public delegate Task PromptValidator(IBotContext context, InT toValidate); + } +} \ No newline at end of file diff --git a/libraries/Microsoft.Bot.Builder.Prompts/RangePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/RangePrompt.cs new file mode 100644 index 000000000..5c5e9ca1e --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/RangePrompt.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + public class RangeResult + { + public float Start { get; set; } + + public float End { get; set; } + + public string Text { get; set; } + } + + /// + /// RangeResult recognizes range expressions like "between 2 and 4" -> Start =2 End =4 + /// + public class RangePrompt : BasePrompt + { + private IModel _model; + + public RangePrompt(string culture, PromptValidator validator = null) + :base(validator) + { + _model = NumberRecognizer.Instance.GetNumberRangeModel(culture); + } + + protected RangePrompt(IModel model, PromptValidator validator = null) + :base(validator) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + } + + public override async Task Recognize(IBotContext context) + { + BotAssert.ContextNotNull(context); + BotAssert.ActivityNotNull(context.Request); + if (context.Request.Type != ActivityTypes.Message) + throw new InvalidOperationException("No Message to Recognize"); + + IMessageActivity message = context.Request.AsMessageActivity(); + var results = _model.Parse(message.Text); + if (results.Any()) + { + var result = results.First(); + if (result.TypeName == "numberrange") + { + string[] values = result.Resolution["value"].ToString().Trim('(', ')').Split(','); + RangeResult rangeResult = new RangeResult() + { + Text = result.Text + }; + if (float.TryParse(values[0], out float startValue) && float.TryParse(values[1], out float endValue)) + { + rangeResult.Start = startValue; + rangeResult.End = endValue; + } + if (await Validate(context, rangeResult)) + return rangeResult; + } + } + return null; + } + + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/TemperaturePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/TemperaturePrompt.cs new file mode 100644 index 000000000..1888bdf91 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/TemperaturePrompt.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + + /// + /// TemperaturePrompt recognizes temperature expressions + /// + public class TemperaturePrompt : NumberWithUnitPrompt + { + public TemperaturePrompt(string culture, PromptValidator validator = null) + : base(NumberWithUnitRecognizer.Instance.GetTemperatureModel(culture), validator) + { + } + + protected TemperaturePrompt(IModel model, PromptValidator validator = null) + : base(model, validator) + { + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/TextPrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/TextPrompt.cs new file mode 100644 index 000000000..a0ec60c4d --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/TextPrompt.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + /// + /// Text Prompt provides a simple mechanism to send text to a user + /// and validate a response. The default validator passes on any + /// non-whitespace string. That behavior is easily changed by deriving + /// from this class and authoring custom validation behavior. + /// + /// For simple validation changes, a PromptValidator may be passed in to the + /// constructor. If the standard validation passes, the custom PromptValidator + /// will be called. + /// + public class TextPrompt : BasePrompt + { + + /// + /// Creates a new instance of a TextPrompt allowing a custom validator + /// to be specified. The custom validator will ONLY be called if the + /// Validate method on the class first passes. + /// + public TextPrompt(PromptValidator validator = null) + :base(validator) + { + } + + /// + /// Used to validate the incoming text, expected on context.Request, is + /// valid according to the rules defined in the validation steps. + /// + public override async Task Recognize(IBotContext context) + { + BotAssert.ContextNotNull(context); + BotAssert.ActivityNotNull(context.Request); + if (context.Request.Type != ActivityTypes.Message) + throw new InvalidOperationException("No Message to Recognize"); + + IMessageActivity message = context.Request.AsMessageActivity(); + if (await Validate(context, message.Text)) + return message.Text; + return null; + } + + } +} diff --git a/libraries/Microsoft.Bot.Builder.Prompts/ValuePrompt.cs b/libraries/Microsoft.Bot.Builder.Prompts/ValuePrompt.cs new file mode 100644 index 000000000..cbedc14dd --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Prompts/ValuePrompt.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.Recognizers.Text.Number; +using Microsoft.Recognizers.Text.NumberWithUnit; +using static Microsoft.Bot.Builder.Prompts.PromptValidatorEx; + +namespace Microsoft.Bot.Builder.Prompts +{ + public class ValueResult + { + public ValueResult() { } + + public string Value { get; set; } + + public string Text { get; set; } + } + + /// + /// CurrencyPrompt recognizes currency expressions as float type + /// + public class ValuePrompt : BasePrompt + { + private IModel _model; + + + protected ValuePrompt(IModel model, PromptValidator validator = null) : base(validator) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + } + + /// + /// Used to validate the incoming text, expected on context.Request, is + /// valid according to the rules defined in the validation steps. + /// + public override async Task Recognize(IBotContext context) + { + BotAssert.ContextNotNull(context); + BotAssert.ActivityNotNull(context.Request); + if (context.Request.Type != ActivityTypes.Message) + throw new InvalidOperationException("No Message to Recognize"); + + IMessageActivity message = context.Request.AsMessageActivity(); + var results = _model.Parse(message.Text); + if (results.Any()) + { + var result = results.First(); + ValueResult value = new ValueResult() + { + Text = result.Text, + Value = (string)result.Resolution["value"] + }; + + if (await Validate(context, value)) + return value; + } + return null; + } + } +} diff --git a/samples/Microsoft.Bot.Samples.Ai.QnA/Microsoft.Bot.Samples.Ai.QnA.csproj b/samples/Microsoft.Bot.Samples.Ai.QnA/Microsoft.Bot.Samples.Ai.QnA.csproj index 2be7c207a..3cf721cc1 100644 --- a/samples/Microsoft.Bot.Samples.Ai.QnA/Microsoft.Bot.Samples.Ai.QnA.csproj +++ b/samples/Microsoft.Bot.Samples.Ai.QnA/Microsoft.Bot.Samples.Ai.QnA.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/AgePromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/AgePromptTests.cs new file mode 100644 index 000000000..86d665b40 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/AgePromptTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Age Prompts")] + public class AgePromptTests + { + [TestMethod] + public async Task AgePrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new AgePrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsNotNull(result.Unit); + Assert.IsInstanceOfType(result.Value, typeof(float)); + context.Reply($"{result.Value} {result.Unit}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("I am 30 years old") + .AssertReply("30 Year") + .StartTest(); + } + + [TestMethod] + public async Task AgePrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new AgePrompt(Culture.English, async (ctx, result) => result.Value > 10); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + context.Reply($"{result.Value} {result.Unit}"); + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send(" it is 1 year old") + .AssertReply("null") + .Send(" it is 15 year old") + .AssertReply("15 Year") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/CurrencyPromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/CurrencyPromptTests.cs new file mode 100644 index 000000000..7ce75ec73 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/CurrencyPromptTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Currency Prompts")] + public class CurrencyPromptTests + { + [TestMethod] + public async Task CurrencyPrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new CurrencyPrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsNotNull(result.Unit); + Assert.IsInstanceOfType(result.Value, typeof(float)); + context.Reply($"{result.Value} {result.Unit}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send(" I would like $45.50") + .AssertReply("45.5 Dollar") + .StartTest(); + } + + [TestMethod] + public async Task CurrencyPrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new CurrencyPrompt(Culture.English, async (ctx, result) => result.Value > 10); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + context.Reply($"{result.Value} {result.Unit}"); + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send(" I would like $1.00") + .AssertReply("null") + .Send(" I would like $45.50") + .AssertReply("45.5 Dollar") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/DimensionPromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/DimensionPromptTests.cs new file mode 100644 index 000000000..9c1422bdb --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/DimensionPromptTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Dimension Prompts")] + public class DimensionPromptTests + { + [TestMethod] + public async Task DimensionPrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new DimensionPrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsNotNull(result.Unit); + Assert.IsInstanceOfType(result.Value, typeof(float)); + context.Reply($"{result.Value} {result.Unit}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("I am 4 feet wide") + .AssertReply("4 Foot") + .Send(" it is 1 foot wide") + .AssertReply("1 Foot") + .StartTest(); + } + + [TestMethod] + public async Task DimensionPrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new DimensionPrompt(Culture.English, async (ctx, result) => result.Value > 10); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + context.Reply($"{result.Value} {result.Unit}"); + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send(" it is 1 foot wide") + .AssertReply("null") + .Send(" it is 40 feet wide") + .AssertReply("40 Foot") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/Microsoft.Bot.Builder.Prompts.Tests.csproj b/tests/Microsoft.Bot.Builder.Prompts.Tests/Microsoft.Bot.Builder.Prompts.Tests.csproj new file mode 100644 index 000000000..cb2cb35e6 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/Microsoft.Bot.Builder.Prompts.Tests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.0 + false + + + + 1701;1702;1705;1998 + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/NumberPromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/NumberPromptTests.cs new file mode 100644 index 000000000..32e2297a7 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/NumberPromptTests.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + public class TestState : StoreItem + { + public bool InPrompt { get; set; } = false; + } + + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Number Prompts")] + public class NumberPromptTests + { + [TestMethod] + public async Task NumberPrompt_Float() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new NumberPrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsInstanceOfType(result.Value, typeof(float)); + context.Reply(result.Value.ToString()); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("asdf df 123") + .AssertReply("123") + .Send(" asdf asd 123.43 adsfsdf ") + .AssertReply("123.43") + .StartTest(); + } + + [TestMethod] + public async Task NumberPrompt_Int() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new NumberPrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsInstanceOfType(result.Value, typeof(int)); + Assert.IsNotNull(result.Text); + context.Reply(result.Value.ToString()); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("asdf df 123") + .AssertReply("123") + .Send(" asdf asd 123.43 adsfsdf ") + .AssertReply("null") + .StartTest(); + } + + [TestMethod] + public async Task NumberPrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new NumberPrompt(Culture.English, async (ctx, result) => result.Value < 100); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsInstanceOfType(result.Value, typeof(int)); + Assert.IsTrue(result.Value < 100); + Assert.IsNotNull(result.Text); + context.Reply(result.Value.ToString()); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("asdf df 123") + .AssertReply("null") + .Send(" asdf asd 12 adsfsdf ") + .AssertReply("12") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/OrdinalPromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/OrdinalPromptTests.cs new file mode 100644 index 000000000..c324afcde --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/OrdinalPromptTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Ordinal Prompts")] + public class OrdinalPromptTests + { + [TestMethod] + public async Task OrdinalPrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new OrdinalPrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsInstanceOfType(result.Value, typeof(int)); + context.Reply(result.Value.ToString()); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send(" the second one please ") + .AssertReply("2") + .StartTest(); + } + + [TestMethod] + public async Task OrdinalPrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new OrdinalPrompt(Culture.English, async (ctx, result) => result.Value > 2); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsInstanceOfType(result.Value, typeof(int)); + Assert.IsTrue(result.Value < 100); + Assert.IsNotNull(result.Text); + context.Reply(result.Value.ToString()); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("the first one") + .AssertReply("null") + .Send("the third one") + .AssertReply("3") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/PercentagePromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/PercentagePromptTests.cs new file mode 100644 index 000000000..6fbd5c0e0 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/PercentagePromptTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Percentage Prompts")] + public class PercentagePromptTests + { + [TestMethod] + public async Task PercentagePrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new PercentagePrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsInstanceOfType(result.Value, typeof(float)); + context.Reply($"{result.Value}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("give me 5") + .AssertReply("null") + .Send(" I would like forty five percent") + .AssertReply("45") + .StartTest(); + } + + [TestMethod] + public async Task PercentagePrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new PercentagePrompt(Culture.English, async (ctx, result) => result.Value > 10); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + context.Reply($"{result.Value}"); + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send(" I would like 5%") + .AssertReply("null") + .Send(" I would like 30%") + .AssertReply("30") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/PhoneNumberPromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/PhoneNumberPromptTests.cs new file mode 100644 index 000000000..bcf210025 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/PhoneNumberPromptTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("PhoneNumber Prompts")] + public class PhoneNumberPromptTests + { + [TestMethod] + public async Task PhoneNumberPrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new PhoneNumberPrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsNotNull(result.Text); + Assert.IsNotNull(result.Value); + context.Reply($"{result.Value}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("123 123123sdfsdf 123 1asdf23123 123 ") + .AssertReply("null") + .Send("123-456-7890") + .AssertReply("123-456-7890") + .StartTest(); + } + + [TestMethod] + public async Task PhoneNumberPrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new PhoneNumberPrompt(Culture.English, async (ctx, result) => result.Value.StartsWith("123")); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + context.Reply($"{result.Value}"); + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("888-123-4567") + .AssertReply("null") + .Send("123-123-4567") + .AssertReply("123-123-4567") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/RangePromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/RangePromptTests.cs new file mode 100644 index 000000000..683677539 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/RangePromptTests.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Range Prompts")] + public class RangePromptTests + { + [TestMethod] + public async Task RangePrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new RangePrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Start > 0); + Assert.IsTrue(result.End > result.Start); + Assert.IsNotNull(result.Text); + context.Reply($"{result.Start}-{result.End}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send("give me 5 10") + .AssertReply("null") + .Send(" give me between 5 and 10") + .AssertReply("5-10") + .StartTest(); + } + + [TestMethod] + public async Task RangePrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new RangePrompt(Culture.English, async (c, result) => result.End - result.Start > 5); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Start > 0); + Assert.IsTrue(result.End > result.Start); + Assert.IsNotNull(result.Text); + context.Reply($"{result.Start}-{result.End}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("give me between 1 and 4") + .AssertReply("null") + .Send(" give me between 1 and 10") + .AssertReply("1-10") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/TemperaturePromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/TemperaturePromptTests.cs new file mode 100644 index 000000000..0836116a1 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/TemperaturePromptTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Temperature Prompts")] + public class TemperaturePromptTests + { + [TestMethod] + public async Task TemperaturePrompt_Test() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var testPrompt = new TemperaturePrompt(Culture.English); + if (!state.InPrompt) + { + state.InPrompt = true; + await testPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await testPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + { + Assert.IsTrue(result.Value != float.NaN); + Assert.IsNotNull(result.Text); + Assert.IsNotNull(result.Unit); + Assert.IsInstanceOfType(result.Value, typeof(float)); + context.Reply($"{result.Value} {result.Unit}"); + } + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send("test test test") + .AssertReply("null") + .Send(" it is 43 degrees") + .AssertReply("43 Degree") + .StartTest(); + } + + [TestMethod] + public async Task TemperaturePrompt_Validator() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, async (context) => + { + var state = ConversationState.Get(context); + var numberPrompt = new TemperaturePrompt(Culture.English, async (ctx, result) => result.Value > 10); + if (!state.InPrompt) + { + state.InPrompt = true; + await numberPrompt.Prompt(context, "Gimme:"); + } + else + { + var result = await numberPrompt.Recognize(context); + if (result == null) + context.Reply("null"); + else + context.Reply($"{result.Value} {result.Unit}"); + } + }) + .Send("hello") + .AssertReply("Gimme:") + .Send(" it is 10 degrees") + .AssertReply("null") + .Send(" it is 43 degrees") + .AssertReply("43 Degree") + .StartTest(); + } + + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Prompts.Tests/TextPromptTests.cs b/tests/Microsoft.Bot.Builder.Prompts.Tests/TextPromptTests.cs new file mode 100644 index 000000000..e2b01c602 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Prompts.Tests/TextPromptTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Middleware; +using Microsoft.Bot.Builder.Storage; +using Microsoft.Bot.Schema; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Bot.Builder.Prompts.Tests +{ + [TestClass] + [TestCategory("Prompts")] + [TestCategory("Text Prompts")] + public class TextPromptTests + { + [TestMethod] + public async Task SimpleRecognize() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, MyTestPrompt) + .Send("hello") + .AssertReply("Your Name:") + .Send("test test test") + .AssertReply("Passed") + .AssertReply("test test test") + .StartTest(); + } + + [TestMethod] + public async Task MinLenghtViaCustomValidator_Fail() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, LengthCheckPromptTest) + .Send("hello") + .AssertReply("Your Name:") + .Send("1") + .AssertReply("Failed") + .StartTest(); + } + [TestMethod] + public async Task MinLenghtViaCustomValidator_Pass() + { + TestAdapter adapter = new TestAdapter() + .Use(new ConversationState(new MemoryStorage())); + + await new TestFlow(adapter, LengthCheckPromptTest) + .Send("hello") + .AssertReply("Your Name:") + .Send("123456") + .AssertReply("Passed") + .AssertReply("123456") + .StartTest(); + } + + + public async Task MyTestPrompt(IBotContext context) + { + dynamic conversationState = ConversationState.Get(context); + TextPrompt askForName = new TextPrompt(); + if (conversationState["topic"] != "textPromptTest") + { + conversationState["topic"] = "textPromptTest"; + await askForName.Prompt(context, "Your Name:"); + } + else + { + var text = await askForName.Recognize(context); + if (text != null) + { + context.Reply("Passed"); + context.Reply(text); + } + else + { + context.Reply("Failed"); + } + } + } + + public async Task LengthCheckPromptTest(IBotContext context) + { + dynamic conversationState = ConversationState.Get(context); + TextPrompt askForName = new TextPrompt(MinLengthValidator); + if (conversationState["topic"] != "textPromptTest") + { + conversationState["topic"] = "textPromptTest"; + await askForName.Prompt(context, "Your Name:"); + } + else + { + var text = await askForName.Recognize(context); + if (text != null) + { + context.Reply("Passed"); + context.Reply(text); + } + else + { + context.Reply("Failed"); + } + } + } + + public async Task MinLengthValidator(IBotContext context, string toValidate) + { + return toValidate.Length > 5; + } + } +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Tests/Microsoft.Bot.Builder.Tests.csproj b/tests/Microsoft.Bot.Builder.Tests/Microsoft.Bot.Builder.Tests.csproj index 7e5c954c8..8f9c18754 100644 --- a/tests/Microsoft.Bot.Builder.Tests/Microsoft.Bot.Builder.Tests.csproj +++ b/tests/Microsoft.Bot.Builder.Tests/Microsoft.Bot.Builder.Tests.csproj @@ -2,7 +2,6 @@ netcoreapp2.0 - false @@ -19,6 +18,7 @@ +