Skip to content
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 4 commits into from
Mar 17, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
381 changes: 118 additions & 263 deletions libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions libraries/Microsoft.Bot.Builder.AI.LUIS/LuisTelemetryConstants.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.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
Copy link
Member

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?)

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";
}
}
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