-
Notifications
You must be signed in to change notification settings - Fork 486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Telemetry Support for Luis Recognizer #1424
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c52fd8b
TelemetryLoggerMiddleware per https://github.com/daveta/analytics/blo…
daveta 698c9ce
Telemetry support in Luis Recognizer
daveta b6327fc
Make property names consistent with QnA, add ability to override metr…
daveta 9282d57
Incorporate feedback from Sreedhar, Ryan and John
daveta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
libraries/Microsoft.Bot.Builder.AI.LUIS/ITelemetryRecognizer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
// Licensed under the MIT license. | ||
|
||
using System.Collections.Generic; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.Bot.Builder.AI.Luis | ||
{ | ||
/// <summary> | ||
/// Recognizer with Telemetry support. | ||
/// </summary> | ||
public interface ITelemetryRecognizer : IRecognizer | ||
{ | ||
/// <summary> | ||
/// Gets a value indicating whether determines whether to log personal information that came from the user. | ||
/// </summary> | ||
/// <value>If true, will log personal information into the IBotTelemetryClient.TrackEvent method; otherwise the properties will be filtered.</value> | ||
bool LogPersonalInformation { get; } | ||
|
||
/// <summary> | ||
/// Gets the currently configured <see cref="IBotTelemetryClient"/> that logs the LuisResult event. | ||
/// </summary> | ||
/// <value>The <see cref=IBotTelemetryClient"/> being used to log events.</value> | ||
IBotTelemetryClient TelemetryClient { get; } | ||
|
||
/// <summary> | ||
/// Return results of the analysis (suggested intents and entities) using the turn context. | ||
/// </summary> | ||
/// <param name="turnContext">Context object containing information for a single turn of conversation with a user.</param> | ||
/// <param name="telemetryProperties">Additional properties to be logged to telemetry with the LuisResult event.</param> | ||
/// <param name="telemetryMetrics">Additional metrics to be logged to telemetry with the LuisResult event.</param> | ||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param> | ||
/// <returns>The LUIS results of the analysis of the current message text in the current turn's context activity.</returns> | ||
Task<RecognizerResult> RecognizeAsync(ITurnContext turnContext, Dictionary<string, string> telemetryProperties, Dictionary<string, double> telemetryMetrics, CancellationToken cancellationToken = default(CancellationToken)); | ||
|
||
Task<T> RecognizeAsync<T>(ITurnContext turnContext, Dictionary<string, string> telemetryProperties, Dictionary<string, double> telemetryMetrics, CancellationToken cancellationToken = default(CancellationToken)) | ||
where T : IRecognizerConvert, new(); | ||
|
||
new Task<T> RecognizeAsync<T>(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) | ||
where T : IRecognizerConvert, new(); | ||
} | ||
} |
381 changes: 118 additions & 263 deletions
381
libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs
Large diffs are not rendered by default.
Oops, something went wrong.
22 changes: 22 additions & 0 deletions
22
libraries/Microsoft.Bot.Builder.AI.LUIS/LuisTelemetryConstants.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
namespace Microsoft.Bot.Builder.AI.Luis | ||
{ | ||
/// <summary> | ||
/// The IBotTelemetryClient event and property names that logged by default. | ||
/// </summary> | ||
public static class LuisTelemetryConstants | ||
{ | ||
public const string LuisResult = "LuisResult"; // Event name | ||
public const string ApplicationIdProperty = "applicationId"; | ||
public const string IntentProperty = "intent"; | ||
public const string IntentScoreProperty = "intentScore"; | ||
public const string EntitiesProperty = "entities"; | ||
public const string QuestionProperty = "question"; | ||
public const string ActivityIdProperty = "activityId"; | ||
public const string SentimentLabelProperty = "sentimentLabel"; | ||
public const string SentimentScoreProperty = "sentimentScore"; | ||
public const string FromIdProperty = "fromId"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Azure.CognitiveServices.Language.LUIS.Runtime.Models; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.Bot.Builder.AI.Luis | ||
{ | ||
// Utility functions used to extract and transform data from Luis SDK | ||
internal static class LuisUtil | ||
{ | ||
internal const string _metadataKey = "$instance"; | ||
|
||
internal static string NormalizedIntent(string intent) => intent.Replace('.', '_').Replace(' ', '_'); | ||
|
||
internal static IDictionary<string, IntentScore> GetIntents(LuisResult luisResult) | ||
{ | ||
if (luisResult.Intents != null) | ||
{ | ||
return luisResult.Intents.ToDictionary( | ||
i => NormalizedIntent(i.Intent), | ||
i => new IntentScore { Score = i.Score ?? 0 }); | ||
} | ||
else | ||
{ | ||
return new Dictionary<string, IntentScore>() | ||
{ | ||
{ | ||
NormalizedIntent(luisResult.TopScoringIntent.Intent), | ||
new IntentScore() { Score = luisResult.TopScoringIntent.Score ?? 0 } | ||
}, | ||
}; | ||
} | ||
} | ||
|
||
internal static JObject ExtractEntitiesAndMetadata(IList<EntityModel> entities, IList<CompositeEntityModel> compositeEntities, bool verbose) | ||
{ | ||
var entitiesAndMetadata = new JObject(); | ||
if (verbose) | ||
{ | ||
entitiesAndMetadata[_metadataKey] = new JObject(); | ||
} | ||
|
||
var compositeEntityTypes = new HashSet<string>(); | ||
|
||
// We start by populating composite entities so that entities covered by them are removed from the entities list | ||
if (compositeEntities != null && compositeEntities.Any()) | ||
{ | ||
compositeEntityTypes = new HashSet<string>(compositeEntities.Select(ce => ce.ParentType)); | ||
entities = compositeEntities.Aggregate(entities, (current, compositeEntity) => PopulateCompositeEntityModel(compositeEntity, current, entitiesAndMetadata, verbose)); | ||
} | ||
|
||
foreach (var entity in entities) | ||
{ | ||
// we'll address composite entities separately | ||
if (compositeEntityTypes.Contains(entity.Type)) | ||
{ | ||
continue; | ||
} | ||
|
||
AddProperty(entitiesAndMetadata, ExtractNormalizedEntityName(entity), ExtractEntityValue(entity)); | ||
|
||
if (verbose) | ||
{ | ||
AddProperty((JObject)entitiesAndMetadata[_metadataKey], ExtractNormalizedEntityName(entity), ExtractEntityMetadata(entity)); | ||
} | ||
} | ||
|
||
return entitiesAndMetadata; | ||
} | ||
|
||
internal 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)); | ||
} | ||
|
||
internal static JToken ExtractEntityValue(EntityModel entity) | ||
{ | ||
#pragma warning disable IDE0007 // Use implicit type | ||
if (entity.AdditionalProperties == null || !entity.AdditionalProperties.TryGetValue("resolution", out dynamic resolution)) | ||
#pragma warning restore IDE0007 // Use implicit type | ||
{ | ||
return entity.Entity; | ||
} | ||
|
||
if (entity.Type.StartsWith("builtin.datetime.")) | ||
{ | ||
return JObject.FromObject(resolution); | ||
} | ||
else if (entity.Type.StartsWith("builtin.datetimeV2.")) | ||
{ | ||
if (resolution.values == null || resolution.values.Count == 0) | ||
{ | ||
return JArray.FromObject(resolution); | ||
} | ||
|
||
var resolutionValues = (IEnumerable<dynamic>)resolution.values; | ||
var type = resolution.values[0].type; | ||
var timexes = resolutionValues.Select(val => val.timex); | ||
var distinctTimexes = timexes.Distinct(); | ||
return new JObject(new JProperty("type", type), new JProperty("timex", JArray.FromObject(distinctTimexes))); | ||
} | ||
else | ||
{ | ||
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 resolution.value ?? JArray.FromObject(resolution.values); | ||
} | ||
} | ||
} | ||
|
||
internal static JObject ExtractEntityMetadata(EntityModel entity) | ||
{ | ||
dynamic obj = JObject.FromObject(new | ||
{ | ||
startIndex = (int)entity.StartIndex, | ||
endIndex = (int)entity.EndIndex + 1, | ||
text = entity.Entity, | ||
type = entity.Type, | ||
}); | ||
if (entity.AdditionalProperties != null) | ||
{ | ||
if (entity.AdditionalProperties.TryGetValue("score", out var score)) | ||
{ | ||
obj.score = (double)score; | ||
} | ||
|
||
#pragma warning disable IDE0007 // Use implicit type | ||
if (entity.AdditionalProperties.TryGetValue("resolution", out dynamic resolution) && resolution.subtype != null) | ||
#pragma warning restore IDE0007 // Use implicit type | ||
{ | ||
obj.subtype = resolution.subtype; | ||
} | ||
} | ||
|
||
return obj; | ||
} | ||
|
||
internal static string ExtractNormalizedEntityName(EntityModel entity) | ||
{ | ||
// Type::Role -> Role | ||
var type = entity.Type.Split(':').Last(); | ||
if (type.StartsWith("builtin.datetimeV2.")) | ||
{ | ||
type = "datetime"; | ||
} | ||
|
||
if (type.StartsWith("builtin.currency")) | ||
{ | ||
type = "money"; | ||
} | ||
|
||
if (type.StartsWith("builtin.")) | ||
{ | ||
type = type.Substring(8); | ||
} | ||
|
||
var role = entity.AdditionalProperties != null && entity.AdditionalProperties.ContainsKey("role") ? (string)entity.AdditionalProperties["role"] : string.Empty; | ||
if (!string.IsNullOrWhiteSpace(role)) | ||
{ | ||
type = role; | ||
} | ||
|
||
return type.Replace('.', '_').Replace(' ', '_'); | ||
} | ||
|
||
internal static IList<EntityModel> PopulateCompositeEntityModel(CompositeEntityModel compositeEntity, IList<EntityModel> entities, JObject entitiesAndMetadata, bool verbose) | ||
{ | ||
var childrenEntites = new JObject(); | ||
var childrenEntitiesMetadata = new JObject(); | ||
if (verbose) | ||
{ | ||
childrenEntites[_metadataKey] = new JObject(); | ||
} | ||
|
||
// This is now implemented as O(n^2) search and can be reduced to O(2n) using a map as an optimization if n grows | ||
var compositeEntityMetadata = entities.FirstOrDefault(e => e.Type == compositeEntity.ParentType && e.Entity == compositeEntity.Value); | ||
|
||
// This is an error case and should not happen in theory | ||
if (compositeEntityMetadata == null) | ||
{ | ||
return entities; | ||
} | ||
|
||
if (verbose) | ||
{ | ||
childrenEntitiesMetadata = ExtractEntityMetadata(compositeEntityMetadata); | ||
childrenEntites[_metadataKey] = new JObject(); | ||
} | ||
|
||
var coveredSet = new HashSet<EntityModel>(); | ||
foreach (var child in compositeEntity.Children) | ||
{ | ||
foreach (var entity in entities) | ||
{ | ||
// We already covered this entity | ||
if (coveredSet.Contains(entity)) | ||
{ | ||
continue; | ||
} | ||
|
||
// This entity doesn't belong to this composite entity | ||
if (child.Type != entity.Type || !CompositeContainsEntity(compositeEntityMetadata, entity)) | ||
{ | ||
continue; | ||
} | ||
|
||
// 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, ExtractNormalizedEntityName(entity), ExtractEntityValue(entity)); | ||
|
||
if (verbose) | ||
{ | ||
AddProperty((JObject)childrenEntites[_metadataKey], ExtractNormalizedEntityName(entity), ExtractEntityMetadata(entity)); | ||
} | ||
} | ||
} | ||
|
||
AddProperty(entitiesAndMetadata, ExtractNormalizedEntityName(compositeEntityMetadata), childrenEntites); | ||
if (verbose) | ||
{ | ||
AddProperty((JObject)entitiesAndMetadata[_metadataKey], ExtractNormalizedEntityName(compositeEntityMetadata), childrenEntitiesMetadata); | ||
} | ||
|
||
// filter entities that were covered by this composite entity | ||
return entities.Except(coveredSet).ToList(); | ||
} | ||
|
||
internal static bool CompositeContainsEntity(EntityModel compositeEntityMetadata, EntityModel entity) | ||
=> entity.StartIndex >= compositeEntityMetadata.StartIndex && | ||
entity.EndIndex <= compositeEntityMetadata.EndIndex; | ||
|
||
/// <summary> | ||
/// If a property doesn't exist add it to a new array, otherwise append it to the existing array. | ||
/// </summary> | ||
internal static void AddProperty(JObject obj, string key, JToken value) | ||
{ | ||
if (((IDictionary<string, JToken>)obj).ContainsKey(key)) | ||
{ | ||
((JArray)obj[key]).Add(value); | ||
} | ||
else | ||
{ | ||
obj[key] = new JArray(value); | ||
} | ||
} | ||
|
||
internal static void AddProperties(LuisResult luis, RecognizerResult result) | ||
{ | ||
if (luis.SentimentAnalysis != null) | ||
{ | ||
result.Properties.Add("sentiment", new JObject( | ||
new JProperty("label", luis.SentimentAnalysis.Label), | ||
new JProperty("score", luis.SentimentAnalysis.Score))); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
namespace Microsoft.Bot.Builder | ||
{ | ||
public static class TelemetryConstants | ||
{ | ||
public const string ChannelIdProperty = "channelId"; | ||
public const string ConversationIdProperty = "conversationId"; | ||
public const string ConversationNameProperty = "conversationName"; | ||
public const string DialogIdProperty = "DialogId"; | ||
public const string FromIdProperty = "fromId"; | ||
public const string FromNameProperty = "fromName"; | ||
public const string LocaleProperty = "locale"; | ||
public const string RecipientIdProperty = "recipientId"; | ||
public const string RecipientNameProperty = "recipientName"; | ||
public const string ReplyActivityIDProperty = "replyActivityId"; | ||
public const string TextProperty = "text"; | ||
public const string SpeakProperty = "speak"; | ||
public const string UserIdProperty = "userId"; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could these be static readonly? (i.e. do you ever use then in a switch-case statement?)