diff --git a/Microsoft.Bot.Builder.sln b/Microsoft.Bot.Builder.sln index e8760b4491..ebe7fffb5c 100644 --- a/Microsoft.Bot.Builder.sln +++ b/Microsoft.Bot.Builder.sln @@ -268,8 +268,8 @@ Global {8D6F4046-0971-4E24-8E4D-F8175C6DFF2B}.Documentation|Any CPU.ActiveCfg = Documentation|Any CPU {8D6F4046-0971-4E24-8E4D-F8175C6DFF2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D6F4046-0971-4E24-8E4D-F8175C6DFF2B}.Release|Any CPU.Build.0 = Release|Any CPU - {183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug - NuGet Packages|Any CPU - {183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.Build.0 = Debug - NuGet Packages|Any CPU + {183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU {183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {183B3324-4CFF-477A-8EAE-73953D3E383D}.Debug|Any CPU.Build.0 = Debug|Any CPU {183B3324-4CFF-477A-8EAE-73953D3E383D}.Documentation|Any CPU.ActiveCfg = Documentation|Any CPU diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/DateTimeExpression.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/DateTimeExpression.cs new file mode 100644 index 0000000000..3d7dd25edb --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/DateTimeExpression.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Bot.Builder.Ai.LUIS +{ + /// + /// Type for LUIS builtin_datetime. + /// + /// + /// LUIS recognizes time expressions like "next monday" and converts those to a type and set of timex expressions. + /// More information on timex can be found here: http://www.timeml.org/publications/timeMLdocs/timeml_1.2.1.html#timex3 + /// More information on the library which does the recognition can be found here: https://github.com/Microsoft/Recognizers-Text + /// + public class DateTimeSpec + { + /// + /// Type of expression. + /// + /// Example types include: + /// + /// time -- simple time expression like "3pm". + /// date -- simple date like "july 3rd". + /// datetime -- combination of date and time like "march 23 2pm". + /// timerange -- a range of time like "2pm to 4pm". + /// daterange -- a range of dates like "march 23rd to 24th". + /// datetimerang -- a range of dates and times like "july 3rd 2pm to 5th 4pm". + /// set -- a recurrence like "every monday". + /// + /// + [JsonProperty("type")] + public readonly string Type; + + /// + /// Timex expressions. + /// + [JsonProperty("timex")] + public readonly IList Expressions; + + public DateTimeSpec(string type, IEnumerable expressions) + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentNullException(nameof(type)); + if (expressions == null) throw new ArgumentNullException(nameof(expressions)); + Type = type; + Expressions = expressions.ToList(); + } + + public override string ToString() + { + return $"DateTimeSpec({Type}, [{String.Join(", ", Expressions)}]"; + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/InstanceData.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/InstanceData.cs new file mode 100644 index 0000000000..8a1a55c7ec --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/InstanceData.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Ai.LUIS +{ + /// + /// Strongly typed information corresponding to LUIS $instance value. + /// + public class InstanceData + { + /// + /// 0-based index in the analyzed text for where entity starts. + /// + [JsonProperty("startIndex")] + public int StartIndex; + + /// + /// 0-based index of the first character beyond the recognized entity. + /// + [JsonProperty("endIndex")] + public int EndIndex; + + /// + /// Word broken and normalized text for the entity. + /// + [JsonProperty("text")] + public string Text; + + /// + /// Optional confidence in the recognition. + /// + [JsonProperty("score")] + public double? Score; + } +} diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/NumberWithUnit.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/NumberWithUnit.cs new file mode 100644 index 0000000000..1f5c68a743 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Generator/NumberWithUnit.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Ai.LUIS +{ + /// + /// Strongly typed class for LUIS number and unit entity recognition. + /// + /// + /// Specific subtypes of this class are generated to match the builtin age, currency, dimension and temperature entities. + /// + public class NumberWithUnit + { + /// + /// Recognized number, or null if unit only. + /// + [JsonProperty("number")] + public readonly double? Number; + + /// + /// Normalized recognized unit. + /// + [JsonProperty("units")] + public readonly string Units; + + public NumberWithUnit(double? number, string units) + { + Number = number; + Units = units; + } + } + + /// + /// Strongly typed LUIS builtin_age. + /// + public class Age: NumberWithUnit + { + public Age(double number, string units) : base(number, units) { } + + public override string ToString() => $"Age({Number} {Units})"; + } + + /// + /// Strongly typed LUIS builtin_dimension. + /// + public class Dimension: NumberWithUnit + { + public Dimension(double number, string units) : base(number, units) { } + public override string ToString() => $"Dimension({Number} {Units})"; + } + + /// + /// Strongly typed LUIS builtin_money. + /// + public class Money : NumberWithUnit + { + public Money(double number, string units) : base(number, units) { } + public override string ToString() => $"Currency({Number} {Units})"; + } + + /// + /// Strongly typed LUIS builtin_temperature. + /// + public class Temperature : NumberWithUnit + { + public Temperature(double number, string units) : base(number, units) { } + public override string ToString() => $"Temperature({Number} {Units})"; + } +} diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/ILuisRecognizer.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/ILuisRecognizer.cs deleted file mode 100644 index 29afa4984e..0000000000 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/ILuisRecognizer.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Cognitive.LUIS.Models; - -namespace Microsoft.Bot.Builder.Ai.LUIS -{ - /// - /// - /// A Luis specific interface that extends the generic Recognizer interface - /// - internal interface ILuisRecognizer : IRecognizer - { - Task<(RecognizerResult recognizerResult, LuisResult luisResult)> CallAndRecognize(string utterance, CancellationToken ct); - } -} diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/IRecognizer.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/IRecognizer.cs deleted file mode 100644 index 2e7dc25678..0000000000 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/IRecognizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Bot.Builder.Ai.LUIS -{ - /// - /// Interface for Recognizers. - /// This should be moved to the Core Bot Builder Library once it's stable enough - /// - public interface IRecognizer - { - /// - /// Runs an utterance through a recognizer and returns the recognizer results - /// - /// utterance - /// cancellation token - /// Recognizer Results - Task Recognize(string utterance, CancellationToken ct); - } -} diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizer.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizer.cs index 1192f0ace3..aa99c88aed 100644 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizer.cs +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizer.cs @@ -10,26 +10,27 @@ using Microsoft.Cognitive.LUIS.Models; using Newtonsoft.Json.Linq; using System.Text.RegularExpressions; +using Microsoft.Bot.Builder.Core.Extensions; namespace Microsoft.Bot.Builder.Ai.LUIS { /// /// - /// Provides a LUIS-based implementation for the interface. + /// A LUIS based implementation of IRecognizer. /// - public class LuisRecognizer : ILuisRecognizer + public class LuisRecognizer : IRecognizer { private readonly LuisService _luisService; private readonly ILuisOptions _luisOptions; private readonly ILuisRecognizerOptions _luisRecognizerOptions; private const string MetadataKey = "$instance"; - /// - /// Creates a new object. - /// - /// The LUIS model to use to recognize text. - /// The LUIS recognizer options to use. - /// The LUIS request options to use. + /// + /// Creates a new object. + /// + /// The LUIS model to use to recognize text. + /// The LUIS recognizer options to use. + /// The LUIS request options to use. public LuisRecognizer(ILuisModel luisModel, ILuisRecognizerOptions luisRecognizerOptions = null, ILuisOptions options = null) { _luisService = new LuisService(luisModel); @@ -37,26 +38,22 @@ public LuisRecognizer(ILuisModel luisModel, ILuisRecognizerOptions luisRecognize _luisRecognizerOptions = luisRecognizerOptions ?? new LuisRecognizerOptions { Verbose = true }; } - /// - /// Runs an utterance through a LUIS recognizer and returns the recognizer results. - /// - /// The utterance. - /// A cancellation token. - /// The recognizer results. + /// public async Task Recognize(string utterance, CancellationToken ct) { - var result = await CallAndRecognize(utterance, ct).ConfigureAwait(false); - return result.recognizerResult; + return (await RecognizeInternal(utterance, ct).ConfigureAwait(false)); } - /// - /// Runs an utterance through a LUIS recognizer and returns the recognizer results. - /// - /// The utterance. - /// A cancellation token. - /// The recognizer results and LUIS result. - /// This method adds metadata to the recognizer's results. - public Task<(RecognizerResult recognizerResult, LuisResult luisResult)> CallAndRecognize(string utterance, CancellationToken ct) + /// + public async Task Recognize(string utterance, CancellationToken ct) + where T : IRecognizerConvert, new() + { + var result = new T(); + result.Convert(await RecognizeInternal(utterance, ct).ConfigureAwait(false)); + return result; + } + + private Task RecognizeInternal(string utterance, CancellationToken ct) { if (string.IsNullOrEmpty(utterance)) throw new ArgumentNullException(nameof(utterance)); @@ -66,26 +63,18 @@ public async Task Recognize(string utterance, CancellationToke return Recognize(luisRequest, ct, _luisRecognizerOptions.Verbose); } - /// - /// Runs an utterance through a LUIS recognizer and returns the recognizer results. - /// - /// A LUIS request for an utterance. - /// A cancellation token. - /// Whether to add metadata to the recognizer's results. - /// The recognizer results and LUIS result. - private async Task<(RecognizerResult recognizerResult, LuisResult luisResult)> Recognize(LuisRequest request, CancellationToken ct, bool verbose) + private async Task Recognize(LuisRequest request, CancellationToken ct, bool verbose) { var luisResult = await _luisService.QueryAsync(request, ct).ConfigureAwait(false); - var recognizerResult = new RecognizerResult { Text = request.Query, AlteredText = luisResult.AlteredQuery, Intents = GetIntents(luisResult), - Entities = GetEntitiesAndMetadata(luisResult.Entities, luisResult.CompositeEntities, verbose) + Entities = ExtractEntitiesAndMetadata(luisResult.Entities, luisResult.CompositeEntities, verbose), }; - - return (recognizerResult, luisResult); + recognizerResult.Properties.Add("luisResult", luisResult); + return recognizerResult; } private static JObject GetIntents(LuisResult luisResult) @@ -95,7 +84,7 @@ private static JObject GetIntents(LuisResult luisResult) new JObject { [luisResult.TopScoringIntent.Intent] = luisResult.TopScoringIntent.Score ?? 0 }; } - private static JObject GetEntitiesAndMetadata(IList entities, IList compositeEntities, bool verbose) + private static JObject ExtractEntitiesAndMetadata(IList entities, IList compositeEntities, bool verbose) { var entitiesAndMetadata = new JObject(); if (verbose) @@ -117,18 +106,29 @@ private static JObject GetEntitiesAndMetadata(IList entiti if (compositeEntityTypes.Contains(entity.Type)) continue; - AddProperty(entitiesAndMetadata, GetNormalizedEntityType(entity), GetEntityValue(entity)); + AddProperty(entitiesAndMetadata, ExtractNormalizedEntityType(entity), ExtractEntityValue(entity)); if (verbose) { - AddProperty((JObject) entitiesAndMetadata[MetadataKey], GetNormalizedEntityType(entity), GetEntityMetadata(entity)); + AddProperty((JObject)entitiesAndMetadata[MetadataKey], ExtractNormalizedEntityType(entity), ExtractEntityMetadata(entity)); } } return entitiesAndMetadata; } - private static JToken GetEntityValue(EntityRecommendation entity) + private static JToken Number(dynamic value) + { + if (value == null) + { + return null; + } + return long.TryParse((string)value, out var longVal) ? + new JValue(longVal) : + new JValue(double.Parse((string)value)); + } + + private static JToken ExtractEntityValue(EntityRecommendation entity) { if (entity.Resolution == null) return entity.Entity; @@ -138,41 +138,81 @@ private static JToken GetEntityValue(EntityRecommendation entity) if (entity.Resolution?.Values == null || entity.Resolution.Values.Count == 0) return JArray.FromObject(entity.Resolution); - var resolutionValues = (IEnumerable)entity.Resolution.Values.First(); - var timexes = resolutionValues.Select(value => ((IDictionary) value)["timex"]); + var resolutionValues = (IEnumerable)entity.Resolution["values"]; + var type = ((IDictionary)(resolutionValues.First()))["type"]; + var timexes = resolutionValues.Select(val => ((IDictionary)val)["timex"]); var distinctTimexes = timexes.Distinct(); - return JArray.FromObject(distinctTimexes); + return new JObject(new JProperty("type", type), new JProperty("timex", JArray.FromObject(distinctTimexes))); } - - if (entity.Type.StartsWith("builtin.number") || entity.Type.StartsWith("builtin.ordinal")) + else { - var value = (string) entity.Resolution.Values.First(); - return long.TryParse(value, out var longVal) ? - new JValue(longVal) : - new JValue(double.Parse(value)); + var resolution = entity.Resolution; + switch (entity.Type) + { + case "builtin.number": + case "builtin.ordinal": return Number(resolution["value"]); + case "builtin.percentage": + { + var svalue = (string)resolution["value"]; + if (svalue.EndsWith("%")) + { + svalue = svalue.Substring(0, svalue.Length - 1); + } + return Number(svalue); + } + case "builtin.age": + case "builtin.dimension": + case "builtin.currency": + case "builtin.temperature": + { + var units = (string)resolution["unit"]; + var val = Number(resolution["value"]); + var obj = new JObject(); + if (val != null) + { + obj.Add("number", val); + } + obj.Add("units", units); + return obj; + } + default: + return entity.Resolution.Count > 1 ? + JObject.FromObject(entity.Resolution) : + entity.Resolution.ContainsKey("value") ? + (JToken)new JValue(entity.Resolution["value"]) : + JArray.FromObject(entity.Resolution["values"]); + } } - - return entity.Resolution.Count > 1 ? - JObject.FromObject(entity.Resolution) : - entity.Resolution.ContainsKey("value") ? - (JToken) JObject.FromObject(entity.Resolution["value"]) : - JArray.FromObject(entity.Resolution["values"]); } - private static JObject GetEntityMetadata(EntityRecommendation entity) + private static JObject ExtractEntityMetadata(EntityRecommendation entity) { return JObject.FromObject(new { startIndex = entity.StartIndex, - endIndex = entity.EndIndex, + endIndex = entity.EndIndex + 1, text = entity.Entity, score = entity.Score }); } - private static string GetNormalizedEntityType(EntityRecommendation entity) + private static string ExtractNormalizedEntityType(EntityRecommendation entity) { - return Regex.Replace(entity.Type, "\\.", "_"); + // Type::Role -> Role + var type = entity.Type.Split(':').Last(); + if (type.StartsWith("builtin.datetimeV2.")) + { + type = "builtin_datetime"; + } + if (type.StartsWith("builtin.currency")) + { + type = "builtin_money"; + } + if (entity.Role != null) + { + type = entity.Role; + } + return Regex.Replace(type, "\\.", "_"); } private static IList PopulateCompositeEntity(CompositeEntity compositeEntity, IList entities, JObject entitiesAndMetadata, bool verbose) @@ -193,14 +233,14 @@ private static IList PopulateCompositeEntity(CompositeEnti if (verbose) { - childrenEntitiesMetadata = GetEntityMetadata(compositeEntityMetadata); + childrenEntitiesMetadata = ExtractEntityMetadata(compositeEntityMetadata); childrenEntites[MetadataKey] = new JObject(); } - + var coveredSet = new HashSet(); foreach (var child in compositeEntity.Children) { - foreach(var entity in entities) + foreach (var entity in entities) { // We already covered this entity if (coveredSet.Contains(entity)) @@ -212,15 +252,15 @@ private static IList PopulateCompositeEntity(CompositeEnti // Add to the set to ensure that we don't consider the same child entity more than once per composite coveredSet.Add(entity); - AddProperty(childrenEntites, GetNormalizedEntityType(entity), GetEntityValue(entity)); + AddProperty(childrenEntites, ExtractNormalizedEntityType(entity), ExtractEntityValue(entity)); if (verbose) { - AddProperty((JObject)childrenEntites[MetadataKey], GetNormalizedEntityType(entity), GetEntityMetadata(entity)); + AddProperty((JObject)childrenEntites[MetadataKey], ExtractNormalizedEntityType(entity), ExtractEntityMetadata(entity)); } } } - + AddProperty(entitiesAndMetadata, compositeEntity.ParentType, childrenEntites); if (verbose) { @@ -236,7 +276,7 @@ private static bool CompositeContainsEntity(EntityRecommendation compositeEntity return entity.StartIndex >= compositeEntityMetadata.StartIndex && entity.EndIndex <= compositeEntityMetadata.EndIndex; } - + /// /// If a property doesn't exist add it to a new array, otherwise append it to the existing array /// @@ -244,7 +284,7 @@ private static void AddProperty(JObject obj, string key, JToken value) { if (((IDictionary)obj).ContainsKey(key)) { - ((JArray) obj[key]).Add(value); + ((JArray)obj[key]).Add(value); } else { diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizerMiddleware.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizerMiddleware.cs index 2ac0179564..ab6e920029 100644 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizerMiddleware.cs +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisRecognizerMiddleware.cs @@ -4,8 +4,10 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Bot.Builder.Core.Extensions; using Microsoft.Bot.Schema; using Microsoft.Cognitive.LUIS; +using Microsoft.Cognitive.LUIS.Models; namespace Microsoft.Bot.Builder.Ai.LUIS { @@ -36,7 +38,7 @@ public class LuisRecognizerMiddleware : IMiddleware /// public const string Obfuscated = "****"; - private readonly ILuisRecognizer _luisRecognizer; + private readonly IRecognizer _luisRecognizer; private readonly ILuisModel _luisModel; private readonly ILuisOptions _luisOptions; @@ -65,15 +67,15 @@ public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next) if (context.Activity.Type == ActivityTypes.Message) { var utterance = context.Activity.AsMessageActivity().Text; - var result = await _luisRecognizer.CallAndRecognize(utterance, CancellationToken.None).ConfigureAwait(false); - context.Services.Add(LuisRecognizerResultKey, result.recognizerResult); + var result = await _luisRecognizer.Recognize(utterance, CancellationToken.None).ConfigureAwait(false); + context.Services.Add(LuisRecognizerResultKey, result); var traceInfo = new LuisTraceInfo { - RecognizerResult = result.recognizerResult, + RecognizerResult = result, LuisModel = RemoveSensitiveData(_luisModel), LuisOptions = _luisOptions, - LuisResult = result.luisResult + LuisResult = (LuisResult) result.Properties["luisResult"] }; var traceActivity = Activity.CreateTraceActivity("LuisRecognizerMiddleware", LuisTraceType, traceInfo, LuisTraceLabel); await context.SendActivity(traceActivity).ConfigureAwait(false); diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisTraceInfo.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisTraceInfo.cs index 4b7fcfd50f..4a516c2410 100644 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisTraceInfo.cs +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/LuisTraceInfo.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. +using Microsoft.Bot.Builder.Core.Extensions; using Microsoft.Cognitive.LUIS; using Microsoft.Cognitive.LUIS.Models; using Newtonsoft.Json; diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/Microsoft.Bot.Builder.Ai.LUIS.csproj b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Microsoft.Bot.Builder.Ai.LUIS.csproj index fab1378fb7..7e05690e94 100644 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/Microsoft.Bot.Builder.Ai.LUIS.csproj +++ b/libraries/Microsoft.Bot.Builder.Ai.LUIS/Microsoft.Bot.Builder.Ai.LUIS.csproj @@ -56,8 +56,9 @@ - - - - + + + + + diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/RecognizerResult.cs b/libraries/Microsoft.Bot.Builder.Ai.LUIS/RecognizerResult.cs deleted file mode 100644 index 0e7a87e57e..0000000000 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/RecognizerResult.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Bot.Builder.Ai.LUIS -{ - /// - /// Contains intent recognizer results. - /// - public class RecognizerResult - { - /// - /// The query sent to the intent regocnizer. - /// - [JsonProperty("text")] - public string Text { set; get; } - - /// - /// The altered query used by the intent recognizer to extract intent and entities. - /// - [JsonProperty("alteredText")] - public string AlteredText { set; get; } - - /// - /// The intents found in the query text. - /// - [JsonProperty("intents")] - public JObject Intents { get; set; } - - /// - /// The entities found in the query text. - /// - [JsonProperty("entities")] - public JObject Entities { get; set; } - } -} diff --git a/libraries/Microsoft.Bot.Builder.Core.Extensions/IRecognizer.cs b/libraries/Microsoft.Bot.Builder.Core.Extensions/IRecognizer.cs new file mode 100644 index 0000000000..9eb4be2039 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Core.Extensions/IRecognizer.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Bot.Builder.Core.Extensions +{ + /// + /// Can convert from a generic recognizer result to a strongly typed one. + /// + public interface IRecognizerConvert + { + /// + /// Convert recognizer result. + /// + /// Result to convert. + void Convert(dynamic result); + } + + /// + /// Interface for Recognizers. + /// + public interface IRecognizer + { + /// + /// Runs an utterance through a recognizer and returns a generic recognizer result. + /// + /// Utterance to analyze. + /// Cancellation token. + /// Analysis of utterance. + Task Recognize(string utterance, CancellationToken ct); + + /// + /// Runs an utterance through a recognizer and returns a strongly typed recognizer result. + /// + /// Utterance to analyze. + /// Cancellation token. + /// Analysis of utterance. + Task Recognize(string utterance, CancellationToken ct) + where T : IRecognizerConvert, new(); + } +} diff --git a/libraries/Microsoft.Bot.Builder.Core.Extensions/RecognizerResult.cs b/libraries/Microsoft.Bot.Builder.Core.Extensions/RecognizerResult.cs new file mode 100644 index 0000000000..c6ac7daad3 --- /dev/null +++ b/libraries/Microsoft.Bot.Builder.Core.Extensions/RecognizerResult.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace Microsoft.Bot.Builder.Core.Extensions +{ + /// + /// Recognizer return value. + /// + public class RecognizerResult: IRecognizerConvert + { + /// + /// Original text to recognizer. + /// + [JsonProperty("text")] + public string Text { set; get; } + + /// + /// Text modified by recognizer for example by spell correction. + /// + [JsonProperty("alteredText")] + public string AlteredText { set; get; } + + /// + /// Object with the intent as key and the confidence as value. + /// + [JsonProperty("intents")] + public JObject Intents { get; set; } + + /// + /// Object with each top-level recognized entity as a key. + /// + [JsonProperty("entities")] + public JObject Entities { get; set; } + + /// + /// Any extra properties to include in the results. + /// + [JsonExtensionData(ReadData = true, WriteData = true)] + public IDictionary Properties { get; set; } = new Dictionary(); + + /// + public void Convert(dynamic result) + { + Text = result.Text; + AlteredText = result.AlteredText; + Intents = result.Intents; + Entities = result.Entities; + Properties = result.Properties; + } + } +} diff --git a/libraries/Microsoft.Bot.Builder.Ai.LUIS/RecognizerResultExtensions.cs b/libraries/Microsoft.Bot.Builder.Core.Extensions/RecognizerResultExtensions.cs similarity index 57% rename from libraries/Microsoft.Bot.Builder.Ai.LUIS/RecognizerResultExtensions.cs rename to libraries/Microsoft.Bot.Builder.Core.Extensions/RecognizerResultExtensions.cs index 5f87cc98bc..290ae021dd 100644 --- a/libraries/Microsoft.Bot.Builder.Ai.LUIS/RecognizerResultExtensions.cs +++ b/libraries/Microsoft.Bot.Builder.Core.Extensions/RecognizerResultExtensions.cs @@ -1,18 +1,17 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; -namespace Microsoft.Bot.Builder.Ai.LUIS +namespace Microsoft.Bot.Builder.Core.Extensions { - /// - /// Contains methods for working with intent recognizer results. - /// public static class RecognizerResultExtensions { /// - /// Gets the top-scoring intent from recognition results. + /// Return the top scoring intent and its score. /// - /// The recognition results. - /// A tuple of the key the of the top scoring intent and the score associated with that intent. - public static (string key, double score) GetTopScoringIntent(this RecognizerResult result) + /// Recognizer result. + /// Intent and score. + public static (string intent, double score) GetTopScoringIntent(this RecognizerResult result) { if (result == null) throw new ArgumentNullException(nameof(result)); diff --git a/samples/Microsoft.Bot.Samples.Ai.Luis.Translator/LuisTranslatorBot.cs b/samples/Microsoft.Bot.Samples.Ai.Luis.Translator/LuisTranslatorBot.cs index 24cd07387b..f32195f442 100644 --- a/samples/Microsoft.Bot.Samples.Ai.Luis.Translator/LuisTranslatorBot.cs +++ b/samples/Microsoft.Bot.Samples.Ai.Luis.Translator/LuisTranslatorBot.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Ai.LUIS; +using Microsoft.Bot.Builder.Core.Extensions; using Microsoft.Bot.Schema; namespace Microsoft.Bot.Samples.Ai.Luis.Translator diff --git a/samples/Microsoft.Bot.Samples.Ai.Luis/LuisBot.cs b/samples/Microsoft.Bot.Samples.Ai.Luis/LuisBot.cs index 03d10636c6..009544aa06 100644 --- a/samples/Microsoft.Bot.Samples.Ai.Luis/LuisBot.cs +++ b/samples/Microsoft.Bot.Samples.Ai.Luis/LuisBot.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Ai.LUIS; +using Microsoft.Bot.Builder.Core.Extensions; using Microsoft.Bot.Schema; namespace Microsoft.Bot.Samples.Ai.Luis @@ -22,8 +23,8 @@ public async Task OnTurn(ITurnContext context) if (luisResult != null) { - (string key, double score) topItem = luisResult.GetTopScoringIntent(); - await context.SendActivity($"The **top intent** was: **'{topItem.key}'**, with score **{topItem.score}**"); + (string topIntent, double score) = luisResult.GetTopScoringIntent(); + await context.SendActivity($"The **top intent** was: **'{topIntent}'**, with score **{score}**"); await context.SendActivity($"Detail of intents scorings:"); var intentsResult = new List(); diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerMiddlewareTests.cs b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerMiddlewareTests.cs index a6c94cd7bb..92c572376a 100644 --- a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerMiddlewareTests.cs +++ b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerMiddlewareTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Bot.Builder.Adapters; using Microsoft.Bot.Builder.Core.Extensions.Tests; @@ -21,12 +22,6 @@ public class LuisRecognizerMiddlewareTests [TestMethod] public void LuisRecognizer_MiddlewareConstruction() { - if (!EnvironmentVariablesDefined()) - { - Assert.Inconclusive("Missing Luis Environment variables - Skipping test"); - return; - } - var middleware = GetLuisRecognizerMiddleware(); Assert.IsNotNull(middleware); Assert.ThrowsException(() => new LuisRecognizerMiddleware(null)); diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerTests.cs b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerTests.cs index 1f89b9bdb1..150ab83343 100644 --- a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerTests.cs +++ b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisRecognizerTests.cs @@ -3,12 +3,17 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.Bot.Builder.Core.Extensions; using Microsoft.Bot.Builder.Core.Extensions.Tests; using Microsoft.Cognitive.LUIS; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.Bot.Builder.Ai.LUIS.Tests @@ -49,7 +54,7 @@ public async Task SingleIntent_SimplyEntity() Assert.IsNotNull(result.Entities["$instance"]); Assert.IsNotNull(result.Entities["$instance"]["Name"]); Assert.AreEqual(11, (int)result.Entities["$instance"]["Name"].First["startIndex"]); - Assert.AreEqual(14, (int)result.Entities["$instance"]["Name"].First["endIndex"]); + Assert.AreEqual(15, (int)result.Entities["$instance"]["Name"].First["endIndex"]); AssertScore(result.Entities["$instance"]["Name"].First["score"]); } @@ -70,23 +75,23 @@ public async Task MultipleIntents_PrebuiltEntity() Assert.IsTrue(result.Intents.Count > 1); Assert.IsNotNull(result.Intents["Delivery"]); Assert.IsTrue((double)result.Intents["Delivery"] > 0 && (double)result.Intents["Delivery"] <= 1); - Assert.AreEqual("Delivery", result.GetTopScoringIntent().Item1); - Assert.IsTrue(result.GetTopScoringIntent().Item2 > 0); + Assert.AreEqual("Delivery", result.GetTopScoringIntent().intent); + Assert.IsTrue(result.GetTopScoringIntent().score > 0); Assert.IsNotNull(result.Entities); Assert.IsNotNull(result.Entities["builtin_number"]); Assert.AreEqual(2001, (int)result.Entities["builtin_number"].First); Assert.IsNotNull(result.Entities["builtin_ordinal"]); Assert.AreEqual(2, (int)result.Entities["builtin_ordinal"].First); - Assert.IsNotNull(result.Entities["builtin_datetimeV2_date"].First); - Assert.AreEqual("2001-02-02", (string)result.Entities["builtin_datetimeV2_date"].First.First); + Assert.IsNotNull(result.Entities["builtin_datetime"].First); + Assert.AreEqual("2001-02-02", (string)result.Entities["builtin_datetime"].First["timex"].First); Assert.IsNotNull(result.Entities["$instance"]["builtin_number"]); Assert.AreEqual(28, (int)result.Entities["$instance"]["builtin_number"].First["startIndex"]); - Assert.AreEqual(31, (int)result.Entities["$instance"]["builtin_number"].First["endIndex"]); - Assert.AreEqual("2001", (string)result.Entities["$instance"]["builtin_number"].First["text"]); - Assert.IsNotNull(result.Entities["$instance"]["builtin_datetimeV2_date"]); - Assert.AreEqual(15, (int)result.Entities["$instance"]["builtin_datetimeV2_date"].First["startIndex"]); - Assert.AreEqual(31, (int)result.Entities["$instance"]["builtin_datetimeV2_date"].First["endIndex"]); - Assert.AreEqual("february 2nd 2001", (string)result.Entities["$instance"]["builtin_datetimeV2_date"].First["text"]); + Assert.AreEqual(32, (int)result.Entities["$instance"]["builtin_number"].First["endIndex"]); + Assert.AreEqual("2001", result.Text.Substring(28, 32 - 28)); + Assert.IsNotNull(result.Entities["$instance"]["builtin_datetime"]); + Assert.AreEqual(15, (int)result.Entities["$instance"]["builtin_datetime"].First["startIndex"]); + Assert.AreEqual(32, (int)result.Entities["$instance"]["builtin_datetime"].First["endIndex"]); + Assert.AreEqual("february 2nd 2001", (string)result.Entities["$instance"]["builtin_datetime"].First["text"]); } [TestMethod] @@ -110,8 +115,8 @@ public async Task MultipleIntents_PrebuiltEntitiesWithMultiValues() Assert.AreEqual(2, result.Entities["builtin_number"].Count()); Assert.IsTrue(result.Entities["builtin_number"].Any(v => (int)v == 201)); Assert.IsTrue(result.Entities["builtin_number"].Any(v => (int)v == 2001)); - Assert.IsNotNull(result.Entities["builtin_datetimeV2_date"].First); - Assert.AreEqual("2001-02-02", (string)result.Entities["builtin_datetimeV2_date"].First.First); + Assert.IsNotNull(result.Entities["builtin_datetime"].First); + Assert.AreEqual("2001-02-02", (string)result.Entities["builtin_datetime"].First["timex"].First); } [TestMethod] @@ -136,7 +141,7 @@ public async Task MultipleIntents_ListEntityWithSingleValue() Assert.IsNotNull(result.Entities["$instance"]); Assert.IsNotNull(result.Entities["$instance"]["Airline"]); Assert.AreEqual(20, result.Entities["$instance"]["Airline"][0]["startIndex"]); - Assert.AreEqual(25, result.Entities["$instance"]["Airline"][0]["endIndex"]); + Assert.AreEqual(26, result.Entities["$instance"]["Airline"][0]["endIndex"]); Assert.AreEqual("united", result.Entities["$instance"]["Airline"][0]["text"]); } @@ -164,7 +169,7 @@ public async Task MultipleIntents_ListEntityWithMultiValues() Assert.IsNotNull(result.Entities["$instance"]); Assert.IsNotNull(result.Entities["$instance"]["Airline"]); Assert.AreEqual(20, result.Entities["$instance"]["Airline"][0]["startIndex"]); - Assert.AreEqual(21, result.Entities["$instance"]["Airline"][0]["endIndex"]); + Assert.AreEqual(22, result.Entities["$instance"]["Airline"][0]["endIndex"]); Assert.AreEqual("dl", result.Entities["$instance"]["Airline"][0]["text"]); } @@ -195,17 +200,18 @@ public async Task MultipleIntens_CompositeEntity() Assert.IsNull(result.Entities["$instance"]["State"]); Assert.IsNotNull(result.Entities["$instance"]["Address"]); Assert.AreEqual(21, result.Entities["$instance"]["Address"][0]["startIndex"]); - Assert.AreEqual(28, result.Entities["$instance"]["Address"][0]["endIndex"]); + Assert.AreEqual(29, result.Entities["$instance"]["Address"][0]["endIndex"]); AssertScore(result.Entities["$instance"]["Address"][0]["score"]); Assert.IsNotNull(result.Entities["Address"][0]["$instance"]); Assert.IsNotNull(result.Entities["Address"][0]["$instance"]["builtin_number"]); Assert.AreEqual(21, result.Entities["Address"][0]["$instance"]["builtin_number"][0]["startIndex"]); - Assert.AreEqual(25, result.Entities["Address"][0]["$instance"]["builtin_number"][0]["endIndex"]); + Assert.AreEqual(26, result.Entities["Address"][0]["$instance"]["builtin_number"][0]["endIndex"]); Assert.AreEqual("98033", result.Entities["Address"][0]["$instance"]["builtin_number"][0]["text"]); Assert.IsNotNull(result.Entities["Address"][0]["$instance"]["State"]); Assert.AreEqual(27, result.Entities["Address"][0]["$instance"]["State"][0]["startIndex"]); - Assert.AreEqual(28, result.Entities["Address"][0]["$instance"]["State"][0]["endIndex"]); + Assert.AreEqual(29, result.Entities["Address"][0]["$instance"]["State"][0]["endIndex"]); Assert.AreEqual("wa", result.Entities["Address"][0]["$instance"]["State"][0]["text"]); + Assert.AreEqual("WA", result.Text.Substring(27, 29 - 27)); AssertScore(result.Entities["Address"][0]["$instance"]["State"][0]["score"]); } @@ -220,24 +226,142 @@ public async Task MultipleDateTimeEntities() var luisRecognizer = GetLuisRecognizer(verbose: true, luisOptions: new LuisRequest { Verbose = true }); var result = await luisRecognizer.Recognize("Book a table on Friday or tomorrow at 5 or tomorrow at 4", CancellationToken.None); - Assert.IsNotNull(result.Entities["builtin_datetimeV2_date"]); - Assert.AreEqual(1, result.Entities["builtin_datetimeV2_date"].Count()); - Assert.AreEqual(1, result.Entities["builtin_datetimeV2_date"][0].Count()); - Assert.AreEqual("XXXX-WXX-5", (string)result.Entities["builtin_datetimeV2_date"][0][0]); - Assert.AreEqual(2, result.Entities["builtin_datetimeV2_datetime"].Count()); - Assert.AreEqual(2, result.Entities["builtin_datetimeV2_datetime"][0].Count()); - Assert.AreEqual(2, result.Entities["builtin_datetimeV2_datetime"][1].Count()); - Assert.IsTrue(((string)result.Entities["builtin_datetimeV2_datetime"][0][0]).EndsWith("T05")); - Assert.IsTrue(((string)result.Entities["builtin_datetimeV2_datetime"][0][1]).EndsWith("T17")); - Assert.IsTrue(((string)result.Entities["builtin_datetimeV2_datetime"][1][0]).EndsWith("T04")); - Assert.IsTrue(((string)result.Entities["builtin_datetimeV2_datetime"][1][1]).EndsWith("T16")); - Assert.AreEqual(1, result.Entities["$instance"]["builtin_datetimeV2_date"].Count()); - Assert.AreEqual(2, result.Entities["$instance"]["builtin_datetimeV2_datetime"].Count()); + Assert.IsNotNull(result.Entities["builtin_datetime"]); + Assert.AreEqual(3, result.Entities["builtin_datetime"].Count()); + Assert.AreEqual(1, result.Entities["builtin_datetime"][0]["timex"].Count()); + Assert.AreEqual("XXXX-WXX-5", (string)result.Entities["builtin_datetime"][0]["timex"][0]); + Assert.AreEqual(1, result.Entities["builtin_datetime"][0]["timex"].Count()); + Assert.AreEqual(2, result.Entities["builtin_datetime"][1]["timex"].Count()); + Assert.AreEqual(2, result.Entities["builtin_datetime"][2]["timex"].Count()); + Assert.IsTrue(((string)result.Entities["builtin_datetime"][1]["timex"][0]).EndsWith("T05")); + Assert.IsTrue(((string)result.Entities["builtin_datetime"][1]["timex"][1]).EndsWith("T17")); + Assert.IsTrue(((string)result.Entities["builtin_datetime"][2]["timex"][0]).EndsWith("T04")); + Assert.IsTrue(((string)result.Entities["builtin_datetime"][2]["timex"][1]).EndsWith("T16")); + Assert.AreEqual(3, result.Entities["$instance"]["builtin_datetime"].Count()); + } + + // Compare two JSON structures and ensure entity and intent scores are within delta + private bool WithinDelta(JToken token1, JToken token2, double delta, bool compare = false) + { + bool withinDelta = true; + if (token1.Type == JTokenType.Object && token2.Type == JTokenType.Object) + { + var obj1 = (JObject)token1; + var obj2 = (JObject)token2; + withinDelta = obj1.Count == obj2.Count; + foreach (var property in obj1) + { + if (!withinDelta) + { + break; + } + if (obj2.TryGetValue(property.Key, out JToken val2)) + { + withinDelta = WithinDelta(property.Value, val2, delta, compare || property.Key == "score" || property.Key == "intents"); + } + } + } + else if (token1.Type == JTokenType.Array && token2.Type == JTokenType.Array) + { + var arr1 = (JArray)token1; + var arr2 = (JArray)token2; + withinDelta = arr1.Count() == arr2.Count(); + for (var i = 0; withinDelta && i < arr1.Count(); ++i) + { + withinDelta = WithinDelta(arr1[i], arr2[i], delta); + if (!withinDelta) + { + break; + } + } + } + else if (!token1.Equals(token2)) + { + var val1 = (JValue)token1; + var val2 = (JValue)token2; + withinDelta = false; + if (compare && + double.TryParse((string)val1, out double num1) + && double.TryParse((string)val2, out double num2)) + { + withinDelta = Math.Abs(num1 - num2) < delta; + } + } + return withinDelta; + } + + private JObject JsonLuisResult(RecognizerResult result) + { + return new JObject( + new JProperty("alteredText", result.AlteredText), + new JProperty("entities", result.Entities), + new JProperty("intents", result.Intents), + new JProperty("text", result.Text)); + } + + // To create a file to test: + // 1) Create a .json file with an object { text: } in it. + // 2) Run this test which will fail and generate a .json.new file. + // 3) Check the .new file and if correct, replace the original .json file with it. + public async Task TestJson(string file) + { + if (!EnvironmentVariablesDefined()) + { + Assert.Inconclusive("Missing Luis Environment variables - Skipping test"); + return; + } + + var expectedPath = Path.Combine(@"..\..\..\TestData\", file); + var newPath = expectedPath + ".new"; + var luisRecognizer = GetLuisRecognizer(verbose: true, luisOptions: new LuisRequest { Verbose = true }); + var expected = new StreamReader(expectedPath).ReadToEnd(); + dynamic expectedJson = JsonConvert.DeserializeObject(expected); + var result = await luisRecognizer.Recognize((string)expectedJson.text, CancellationToken.None); + var jsonResult = JsonLuisResult(result); + if (!WithinDelta(expectedJson, jsonResult, 0.01)) + { + using (var writer = new StreamWriter(newPath)) + { + writer.Write(jsonResult); + } + Assert.Fail($"Returned JSON in {newPath} != expected JSON in {expectedPath}"); + } + else + { + File.Delete(expectedPath + ".new"); + } + } + + [TestMethod] + public async Task Composite1() + { + await TestJson("Composite1.json"); + } + + [TestMethod] + public async Task Composite2() + { + await TestJson("Composite2.json"); + } + + [TestMethod] + public async Task TypedEntities() + { + if (!EnvironmentVariablesDefined()) + { + Assert.Inconclusive("Missing Luis Environment variables - Skipping test"); + return; + } + var luisRecognizer = GetLuisRecognizer(verbose: true, luisOptions: new LuisRequest { Verbose = true }); + var query = "fly from seattle to dallas"; + var untyped = await luisRecognizer.Recognize(query, CancellationToken.None); + var typed = await luisRecognizer.Recognize(query, CancellationToken.None); + Assert.IsTrue(WithinDelta(JsonLuisResult(untyped), JsonLuisResult(typed), 0.0), "Weakly typed and strongly typed recognize does not match."); } private void AssertScore(JToken scoreToken) { - var score = (double) scoreToken; + var score = (double)scoreToken; Assert.IsTrue(score >= 0); Assert.IsTrue(score <= 1); } diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisTraceInfoTests.cs b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisTraceInfoTests.cs index ba758fb9dc..c2a7b36f0d 100644 --- a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisTraceInfoTests.cs +++ b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/LuisTraceInfoTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using Microsoft.Bot.Builder.Core.Extensions; using Microsoft.Cognitive.LUIS; using Microsoft.Cognitive.LUIS.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/Microsoft.Bot.Builder.Ai.LUIS.Tests.csproj b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/Microsoft.Bot.Builder.Ai.LUIS.Tests.csproj index 5f5ddbabe5..933c390c34 100644 --- a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/Microsoft.Bot.Builder.Ai.LUIS.Tests.csproj +++ b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/Microsoft.Bot.Builder.Ai.LUIS.Tests.csproj @@ -15,12 +15,14 @@ - + + - - + + System + diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/RecognizerResultTests.cs b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/RecognizerResultTests.cs deleted file mode 100644 index 4c06a0c835..0000000000 --- a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/RecognizerResultTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Bot.Builder.Ai.LUIS.Tests -{ - [TestClass] - public class RecognizerResultTests - { - [TestMethod] - public void RecognizerResult_Serialization() - { - const string json = "{\"text\":\"hi there\",\"alteredText\":\"hi there\",\"intents\":{\"Travel\":0.6},\"entities\":{\"Name\":\"Bob\"}}"; - var recognizerResult = new RecognizerResult - { - AlteredText = "hi there", - Text = "hi there", - Entities = JObject.FromObject(new { Name= "Bob" }), - Intents = JObject.FromObject(new { Travel = 0.6f }) - }; - - var serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; - var serialized = JsonConvert.SerializeObject(recognizerResult, serializerSettings); - - Assert.AreEqual(json, serialized); - } - } -} diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/TestData/Composite1.json b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/TestData/Composite1.json new file mode 100644 index 0000000000..ea283f6ed1 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/TestData/Composite1.json @@ -0,0 +1,366 @@ +{ + "alteredText": null, + "entities": { + "$instance": { + "Composite1": [ + { + "startIndex": 0, + "endIndex": 257, + "text": "12 years old and 3 days old and monday july 3rd and every monday and between 3am and 5 : 30am and 4 acres and 4 pico meters and chrimc @ hotmail . com and $ 4 and $ 4 . 25 and 32 and 210 . 4 and first and 10 % and 10 . 5 % and 425 - 555 - 1234 and 3 degrees and - 27 . 5 degrees c", + "score": 0.7393847 + } + ] + }, + "Composite1": [ + { + "$instance": { + "builtin_age": [ + { + "startIndex": 0, + "endIndex": 12, + "text": "12 years old", + "score": null + }, + { + "startIndex": 17, + "endIndex": 27, + "text": "3 days old", + "score": null + } + ], + "builtin_datetime": [ + { + "startIndex": 0, + "endIndex": 8, + "text": "12 years", + "score": null + }, + { + "startIndex": 17, + "endIndex": 23, + "text": "3 days", + "score": null + }, + { + "startIndex": 32, + "endIndex": 47, + "text": "monday july 3rd", + "score": null + }, + { + "startIndex": 52, + "endIndex": 64, + "text": "every monday", + "score": null + }, + { + "startIndex": 69, + "endIndex": 91, + "text": "between 3am and 5:30am", + "score": null + } + ], + "builtin_dimension": [ + { + "startIndex": 96, + "endIndex": 103, + "text": "4 acres", + "score": null + }, + { + "startIndex": 108, + "endIndex": 121, + "text": "4 pico meters", + "score": null + } + ], + "builtin_email": [ + { + "startIndex": 126, + "endIndex": 144, + "text": "chrimc@hotmail.com", + "score": null + } + ], + "builtin_money": [ + { + "startIndex": 149, + "endIndex": 151, + "text": "$4", + "score": null + }, + { + "startIndex": 156, + "endIndex": 161, + "text": "$4.25", + "score": null + } + ], + "builtin_number": [ + { + "startIndex": 0, + "endIndex": 2, + "text": "12", + "score": null + }, + { + "startIndex": 17, + "endIndex": 18, + "text": "3", + "score": null + }, + { + "startIndex": 85, + "endIndex": 86, + "text": "5", + "score": null + }, + { + "startIndex": 96, + "endIndex": 97, + "text": "4", + "score": null + }, + { + "startIndex": 108, + "endIndex": 109, + "text": "4", + "score": null + }, + { + "startIndex": 150, + "endIndex": 151, + "text": "4", + "score": null + }, + { + "startIndex": 157, + "endIndex": 161, + "text": "4.25", + "score": null + }, + { + "startIndex": 166, + "endIndex": 168, + "text": "32", + "score": null + }, + { + "startIndex": 173, + "endIndex": 178, + "text": "210.4", + "score": null + }, + { + "startIndex": 193, + "endIndex": 195, + "text": "10", + "score": null + }, + { + "startIndex": 201, + "endIndex": 205, + "text": "10.5", + "score": null + }, + { + "startIndex": 211, + "endIndex": 214, + "text": "425", + "score": null + }, + { + "startIndex": 215, + "endIndex": 218, + "text": "555", + "score": null + }, + { + "startIndex": 219, + "endIndex": 223, + "text": "1234", + "score": null + }, + { + "startIndex": 228, + "endIndex": 229, + "text": "3", + "score": null + }, + { + "startIndex": 242, + "endIndex": 247, + "text": "-27.5", + "score": null + } + ], + "builtin_ordinal": [ + { + "startIndex": 44, + "endIndex": 47, + "text": "3rd", + "score": null + }, + { + "startIndex": 183, + "endIndex": 188, + "text": "first", + "score": null + } + ], + "builtin_percentage": [ + { + "startIndex": 193, + "endIndex": 196, + "text": "10%", + "score": null + }, + { + "startIndex": 201, + "endIndex": 206, + "text": "10.5%", + "score": null + } + ], + "builtin_phonenumber": [ + { + "startIndex": 211, + "endIndex": 223, + "text": "425-555-1234", + "score": null + } + ], + "builtin_temperature": [ + { + "startIndex": 228, + "endIndex": 237, + "text": "3 degrees", + "score": null + }, + { + "startIndex": 242, + "endIndex": 257, + "text": "-27.5 degrees c", + "score": null + } + ] + }, + "builtin_age": [ + { + "number": 12, + "units": "Year" + }, + { + "number": 3, + "units": "Day" + } + ], + "builtin_datetime": [ + { + "type": "duration", + "timex": [ + "P12Y" + ] + }, + { + "type": "duration", + "timex": [ + "P3D" + ] + }, + { + "type": "date", + "timex": [ + "XXXX-07-03" + ] + }, + { + "type": "set", + "timex": [ + "XXXX-WXX-1" + ] + }, + { + "type": "timerange", + "timex": [ + "(T03,T05:30,PT2H30M)" + ] + } + ], + "builtin_dimension": [ + { + "number": 4, + "units": "Acre" + }, + { + "number": 4, + "units": "Picometer" + } + ], + "builtin_email": [ + "chrimc@hotmail.com" + ], + "builtin_money": [ + { + "number": 4, + "units": "Dollar" + }, + { + "number": 4.25, + "units": "Dollar" + } + ], + "builtin_number": [ + 12, + 3, + 5, + 4, + 4, + 4, + 4.25, + 32, + 210.4, + 10, + 10.5, + 425, + 555, + 1234, + 3, + -27.5 + ], + "builtin_ordinal": [ + 3, + 1 + ], + "builtin_percentage": [ + 10, + 10.5 + ], + "builtin_phonenumber": [ + "425-555-1234" + ], + "builtin_temperature": [ + { + "number": 3, + "units": "Degree" + }, + { + "number": -27.5, + "units": "C" + } + ] + } + ] + }, + "intents": { + "EntityTests": 0.9979519, + "None": 0.03613068, + "Travel": 0.027573321, + "Delivery": 0.0181413647, + "SpecifyName": 0.009525809, + "Help": 0.00179953384, + "Cancel": 0.0005154546, + "Greeting": 4.502414E-06 + }, + "text": "12 years old and 3 days old and monday july 3rd and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c" +} \ No newline at end of file diff --git a/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/TestData/Composite2.json b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/TestData/Composite2.json new file mode 100644 index 0000000000..1b0880ef55 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Ai.LUIS.Tests/TestData/Composite2.json @@ -0,0 +1,76 @@ +{ + "alteredText": null, + "entities": { + "$instance": { + "Composite2": [ + { + "startIndex": 0, + "endIndex": 69, + "text": "http : / / foo . com is where you can fly from seattle to dallas via denver", + "score": 0.8277292 + } + ] + }, + "Composite2": [ + { + "$instance": { + "City": [ + { + "startIndex": 63, + "endIndex": 69, + "text": "denver", + "score": 0.8370886 + } + ], + "builtin_url": [ + { + "startIndex": 0, + "endIndex": 14, + "text": "http://foo.com", + "score": null + } + ], + "From": [ + { + "startIndex": 41, + "endIndex": 48, + "text": "seattle", + "score": 0.987120867 + } + ], + "To": [ + { + "startIndex": 52, + "endIndex": 58, + "text": "dallas", + "score": 0.9829801 + } + ] + }, + "City": [ + "denver" + ], + "builtin_url": [ + "http://foo.com" + ], + "From": [ + "seattle" + ], + "To": [ + "dallas" + ] + } + ] + }, + "intents": { + "EntityTests": 0.9343615, + "Travel": 0.0409771465, + "None": 0.0168768112, + "Delivery": 0.011387079, + "SpecifyName": 0.009291031, + "Help": 0.00648241024, + "Cancel": 0.00276591512, + "Greeting": 0.0002768888 + }, + "text": "http://foo.com is where you can fly from seattle to dallas via denver" +} \ No newline at end of file