Skip to content

Commit

Permalink
Merge pull request #1424 from Microsoft/daveta-telemetry-luis
Browse files Browse the repository at this point in the history
New Telemetry Support for Luis Recognizer
  • Loading branch information
daveta committed Mar 17, 2019
2 parents 106222a + 9282d57 commit ee1c37d
Show file tree
Hide file tree
Showing 9 changed files with 1,419 additions and 262 deletions.
43 changes: 43 additions & 0 deletions libraries/Microsoft.Bot.Builder.AI.LUIS/ITelemetryRecognizer.cs
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();
}
}
383 changes: 121 additions & 262 deletions libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions libraries/Microsoft.Bot.Builder.AI.LUIS/LuisTelemetryConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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 static readonly string LuisResult = "LuisResult"; // Event name
public static readonly string ApplicationIdProperty = "applicationId";
public static readonly string IntentProperty = "intent";
public static readonly string IntentScoreProperty = "intentScore";
public static readonly string Intent2Property = "intent2";
public static readonly string IntentScore2Property = "intentScore2";
public static readonly string EntitiesProperty = "entities";
public static readonly string QuestionProperty = "question";
public static readonly string ActivityIdProperty = "activityId";
public static readonly string SentimentLabelProperty = "sentimentLabel";
public static readonly string SentimentScoreProperty = "sentimentScore";
public static readonly string FromIdProperty = "fromId";
}
}
298 changes: 298 additions & 0 deletions libraries/Microsoft.Bot.Builder.AI.LUIS/LuisUtil.cs
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)));
}
}
}
}
22 changes: 22 additions & 0 deletions libraries/Microsoft.Bot.Builder/TelemetryConstants.cs
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";
}
}
Loading

0 comments on commit ee1c37d

Please sign in to comment.