diff --git a/libraries/Microsoft.Bot.Builder.AI.LUIS/ITelemetryRecognizer.cs b/libraries/Microsoft.Bot.Builder.AI.LUIS/ITelemetryRecognizer.cs
new file mode 100644
index 0000000000..967e466fd6
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder.AI.LUIS/ITelemetryRecognizer.cs
@@ -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
+{
+ ///
+ /// Recognizer with Telemetry support.
+ ///
+ public interface ITelemetryRecognizer : IRecognizer
+ {
+ ///
+ /// Gets a value indicating whether determines whether to log personal information that came from the user.
+ ///
+ /// If true, will log personal information into the IBotTelemetryClient.TrackEvent method; otherwise the properties will be filtered.
+ bool LogPersonalInformation { get; }
+
+ ///
+ /// Gets the currently configured that logs the LuisResult event.
+ ///
+ /// The being used to log events.
+ IBotTelemetryClient TelemetryClient { get; }
+
+ ///
+ /// Return results of the analysis (suggested intents and entities) using the turn context.
+ ///
+ /// Context object containing information for a single turn of conversation with a user.
+ /// Additional properties to be logged to telemetry with the LuisResult event.
+ /// Additional metrics to be logged to telemetry with the LuisResult event.
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// The LUIS results of the analysis of the current message text in the current turn's context activity.
+ Task RecognizeAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary telemetryMetrics, CancellationToken cancellationToken = default(CancellationToken));
+
+ Task RecognizeAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary telemetryMetrics, CancellationToken cancellationToken = default(CancellationToken))
+ where T : IRecognizerConvert, new();
+
+ new Task RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
+ where T : IRecognizerConvert, new();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs b/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs
index 9cc7ccc6fc..5ef2e4bc17 100644
--- a/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs
+++ b/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisRecognizer.cs
@@ -8,7 +8,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.CognitiveServices.Language.LUIS.Runtime;
-using Microsoft.Azure.CognitiveServices.Language.LUIS.Runtime.Models;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Configuration;
using Microsoft.Bot.Schema;
@@ -20,7 +19,7 @@ namespace Microsoft.Bot.Builder.AI.Luis
///
/// A LUIS based implementation of .
///
- public class LuisRecognizer : IRecognizer
+ public class LuisRecognizer : IRecognizer, ITelemetryRecognizer
{
///
/// The value type for a LUIS trace activity.
@@ -31,7 +30,7 @@ public class LuisRecognizer : IRecognizer
/// The context label for a LUIS trace activity.
///
public const string LuisTraceLabel = "Luis Trace";
- private const string _metadataKey = "$instance";
+
private readonly ILUISRuntimeClient _runtime;
private readonly LuisApplication _application;
private readonly LuisPredictionOptions _options;
@@ -44,12 +43,16 @@ public class LuisRecognizer : IRecognizer
/// (Optional) The LUIS prediction options to use.
/// (Optional) TRUE to include raw LUIS API response.
/// (Optional) Custom handler for LUIS API calls to allow mocking.
- public LuisRecognizer(LuisApplication application, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, HttpClientHandler clientHandler = null)
+ /// The IBotTelemetryClient used to log the LuisResult event.
+ public LuisRecognizer(LuisApplication application, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, HttpClientHandler clientHandler = null, IBotTelemetryClient telemetryClient = null, bool logPersonalInformation = false)
{
_application = application ?? throw new ArgumentNullException(nameof(application));
_options = predictionOptions ?? new LuisPredictionOptions();
_includeApiResults = includeApiResults;
+ TelemetryClient = telemetryClient ?? new NullBotTelemetryClient();
+ LogPersonalInformation = logPersonalInformation;
+
var credentials = new ApiKeyServiceClientCredentials(application.EndpointKey);
var delegatingHandler = new LuisDelegatingHandler();
@@ -71,7 +74,7 @@ public LuisRecognizer(LuisApplication application, LuisPredictionOptions predict
/// (Optional) TRUE to include raw LUIS API response.
/// (Optional) Custom handler for LUIS API calls to allow mocking.
public LuisRecognizer(LuisService service, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, HttpClientHandler clientHandler = null)
- : this(new LuisApplication(service), predictionOptions, includeApiResults, clientHandler)
+ : this(new LuisApplication(service), predictionOptions, includeApiResults, clientHandler, null)
{
}
@@ -83,10 +86,36 @@ public LuisRecognizer(LuisService service, LuisPredictionOptions predictionOptio
/// (Optional) TRUE to include raw LUIS API response.
/// (Optional) Custom handler for LUIS API calls to allow mocking.
public LuisRecognizer(string applicationEndpoint, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, HttpClientHandler clientHandler = null)
- : this(new LuisApplication(applicationEndpoint), predictionOptions, includeApiResults, clientHandler)
+ : this(new LuisApplication(applicationEndpoint), predictionOptions, includeApiResults, clientHandler, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The IBotTelemetryClient used to log the LuisResult event.
+ /// The LUIS application to use to recognize text.
+ /// The LUIS prediction options to use.
+ /// TRUE to include raw LUIS API response.
+ /// TRUE to include personally indentifiable information.
+ public LuisRecognizer(LuisApplication application, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, IBotTelemetryClient telemetryClient = null, bool logPersonalInformation = false)
+ : this(application, predictionOptions, includeApiResults, null, telemetryClient)
{
+ LogPersonalInformation = logPersonalInformation;
}
+ ///
+ /// Gets or sets a value indicating whether to log personal information that came from the user to telemetry.
+ ///
+ /// If true, personal information is logged to Telemetry; otherwise the properties will be filtered.
+ public bool LogPersonalInformation { get; set; }
+
+ ///
+ /// Gets the currently configured that logs the LuisResult event.
+ ///
+ /// The being used to log events.
+ public IBotTelemetryClient TelemetryClient { get; }
+
///
/// Returns the name of the top scoring intent from a set of LUIS results.
///
@@ -121,300 +150,127 @@ public static string TopIntent(RecognizerResult results, string defaultIntent =
///
public async Task RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken)
- => await RecognizeInternalAsync(turnContext, cancellationToken).ConfigureAwait(false);
+ => await RecognizeInternalAsync(turnContext, null, null, cancellationToken).ConfigureAwait(false);
///
public async Task RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken)
where T : IRecognizerConvert, new()
{
var result = new T();
- result.Convert(await RecognizeInternalAsync(turnContext, cancellationToken).ConfigureAwait(false));
+ result.Convert(await RecognizeInternalAsync(turnContext, null, null, cancellationToken).ConfigureAwait(false));
return result;
}
- private static string NormalizedIntent(string intent) => intent.Replace('.', '_').Replace(' ', '_');
-
- private static IDictionary GetIntents(LuisResult luisResult)
+ ///
+ /// Return results of the analysis (Suggested actions and intents).
+ ///
+ /// Context object containing information for a single turn of conversation with a user.
+ /// Additional properties to be logged to telemetry with the LuisResult event.
+ /// Additional metrics to be logged to telemetry with the LuisResult event.
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// The LUIS results of the analysis of the current message text in the current turn's context activity.
+ public async Task RecognizeAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary telemetryMetrics = null, CancellationToken cancellationToken = default(CancellationToken))
{
- if (luisResult.Intents != null)
- {
- return luisResult.Intents.ToDictionary(
- i => NormalizedIntent(i.Intent),
- i => new IntentScore { Score = i.Score ?? 0 });
- }
- else
- {
- return new Dictionary()
- {
- {
- NormalizedIntent(luisResult.TopScoringIntent.Intent),
- new IntentScore() { Score = luisResult.TopScoringIntent.Score ?? 0 }
- },
- };
- }
+ return await RecognizeInternalAsync(turnContext, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false);
}
- private static JObject ExtractEntitiesAndMetadata(IList entities, IList compositeEntities, bool verbose)
+ ///
+ /// Return results of the analysis (Suggested actions and intents).
+ ///
+ /// Context object containing information for a single turn of conversation with a user.
+ /// Additional properties to be logged to telemetry with the LuisResult event.
+ /// Additional metrics to be logged to telemetry with the LuisResult event.
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// The LUIS results of the analysis of the current message text in the current turn's context activity.
+ public async Task RecognizeAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary telemetryMetrics = null, CancellationToken cancellationToken = default(CancellationToken))
+ where T : IRecognizerConvert, new()
{
- var entitiesAndMetadata = new JObject();
- if (verbose)
- {
- entitiesAndMetadata[_metadataKey] = new JObject();
- }
-
- var compositeEntityTypes = new HashSet();
-
- // 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(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;
+ var result = new T();
+ result.Convert(await RecognizeInternalAsync(turnContext, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false));
+ return result;
}
- private static JToken Number(dynamic value)
+ ///
+ /// Invoked prior to a LuisResult being logged.
+ ///
+ /// The Luis Results for the call.
+ /// Context object containing information for a single turn of conversation with a user.
+ /// Additional properties to be logged to telemetry with the LuisResult event.
+ /// Additional metrics to be logged to telemetry with the LuisResult event.
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// .
+ protected virtual async Task OnRecognizerResultAsync(RecognizerResult recognizerResult, ITurnContext turnContext, Dictionary telemetryProperties = null, Dictionary telemetryMetrics = null, CancellationToken cancellationToken = default(CancellationToken))
{
- if (value == null)
- {
- return null;
- }
+ var properties = await FillLuisEventPropertiesAsync(recognizerResult, turnContext, telemetryProperties, cancellationToken).ConfigureAwait(false);
- return long.TryParse((string)value, out var longVal) ?
- new JValue(longVal) :
- new JValue(double.Parse((string)value));
+ // Track the event
+ TelemetryClient.TrackEvent(LuisTelemetryConstants.LuisResult, properties, telemetryMetrics);
+
+ return;
}
- private static JToken ExtractEntityValue(EntityModel entity)
+ ///
+ /// Fills the event properties for LuisResult event for telemetry.
+ /// These properties are logged when the recognizer is called.
+ ///
+ /// Last activity sent from user.
+ /// Context object containing information for a single turn of conversation with a user.
+ /// Additional properties to be logged to telemetry with the LuisResult event.
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// additionalProperties
+ /// A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageSend event.
+ protected Task> FillLuisEventPropertiesAsync(RecognizerResult recognizerResult, ITurnContext turnContext, Dictionary telemetryProperties = null, CancellationToken cancellationToken = default(CancellationToken))
{
-#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;
- }
+ var topLuisIntent = recognizerResult.GetTopScoringIntent();
+ var intentScore = topLuisIntent.score.ToString("N2");
+ var topTwoIntents = (recognizerResult.Intents.Count > 0) ? recognizerResult.Intents.OrderByDescending(x => x.Value.Score).Take(2).ToArray() : null;
- if (entity.Type.StartsWith("builtin.datetime."))
- {
- return JObject.FromObject(resolution);
- }
- else if (entity.Type.StartsWith("builtin.datetimeV2."))
+ // Add the intent score and conversation id properties
+ var properties = new Dictionary()
{
- if (resolution.values == null || resolution.values.Count == 0)
- {
- return JArray.FromObject(resolution);
- }
+ { LuisTelemetryConstants.ApplicationIdProperty, _application.ApplicationId },
+ { LuisTelemetryConstants.IntentProperty, topTwoIntents?[0].Key ?? string.Empty },
+ { LuisTelemetryConstants.IntentScoreProperty, topTwoIntents?[0].Value.Score?.ToString("N2") ?? "0.00" },
+ { LuisTelemetryConstants.Intent2Property, (topTwoIntents?.Count() > 1) ? topTwoIntents?[1].Key ?? string.Empty : string.Empty },
+ { LuisTelemetryConstants.IntentScore2Property, (topTwoIntents?.Count() > 1) ? topTwoIntents?[1].Value.Score?.ToString("N2") ?? "0.00" : "0.00" },
+ { LuisTelemetryConstants.FromIdProperty, turnContext.Activity.From.Id },
- var resolutionValues = (IEnumerable)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);
- }
- }
- }
+ };
- private static JObject ExtractEntityMetadata(EntityModel entity)
- {
- dynamic obj = JObject.FromObject(new
+ if (recognizerResult.Properties.TryGetValue("sentiment", out var sentiment) && sentiment is JObject)
{
- 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))
+ if (((JObject)sentiment).TryGetValue("label", out var label))
{
- obj.score = (double)score;
+ properties.Add(LuisTelemetryConstants.SentimentLabelProperty, label.Value());
}
-#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
+ if (((JObject)sentiment).TryGetValue("score", out var score))
{
- obj.subtype = resolution.subtype;
+ properties.Add(LuisTelemetryConstants.SentimentScoreProperty, score.Value());
}
}
- return obj;
- }
-
- private 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";
- }
+ var entities = recognizerResult.Entities?.ToString();
+ properties.Add(LuisTelemetryConstants.EntitiesProperty, entities);
- if (type.StartsWith("builtin."))
+ // Use the LogPersonalInformation flag to toggle logging PII data, text is a common example
+ if (LogPersonalInformation && !string.IsNullOrEmpty(turnContext.Activity.Text))
{
- type = type.Substring(8);
+ properties.Add(LuisTelemetryConstants.QuestionProperty, turnContext.Activity.Text);
}
- var role = entity.AdditionalProperties != null && entity.AdditionalProperties.ContainsKey("role") ? (string)entity.AdditionalProperties["role"] : string.Empty;
- if (!string.IsNullOrWhiteSpace(role))
+ // Additional Properties can override "stock" properties.
+ if (telemetryProperties != null)
{
- type = role;
+ return Task.FromResult(telemetryProperties.Concat(properties)
+ .GroupBy(kv => kv.Key)
+ .ToDictionary(g => g.Key, g => g.First().Value));
}
- return type.Replace('.', '_').Replace(' ', '_');
+ return Task.FromResult(properties);
}
- private static IList PopulateCompositeEntityModel(CompositeEntityModel compositeEntity, IList 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();
- 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();
- }
-
- private static bool CompositeContainsEntity(EntityModel compositeEntityMetadata, EntityModel entity)
- => 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.
- ///
- private static void AddProperty(JObject obj, string key, JToken value)
- {
- if (((IDictionary)obj).ContainsKey(key))
- {
- ((JArray)obj[key]).Add(value);
- }
- else
- {
- obj[key] = new JArray(value);
- }
- }
-
- private 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)));
- }
- }
-
- private async Task RecognizeInternalAsync(ITurnContext turnContext, CancellationToken cancellationToken)
+ private async Task RecognizeInternalAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary telemetryMetrics, CancellationToken cancellationToken)
{
BotAssert.ContextNotNull(turnContext);
@@ -445,15 +301,18 @@ private async Task RecognizeInternalAsync(ITurnContext turnCon
{
Text = utterance,
AlteredText = luisResult.AlteredQuery,
- Intents = GetIntents(luisResult),
- Entities = ExtractEntitiesAndMetadata(luisResult.Entities, luisResult.CompositeEntities, _options.IncludeInstanceData ?? true),
+ Intents = LuisUtil.GetIntents(luisResult),
+ Entities = LuisUtil.ExtractEntitiesAndMetadata(luisResult.Entities, luisResult.CompositeEntities, _options.IncludeInstanceData ?? true),
};
- AddProperties(luisResult, recognizerResult);
+ LuisUtil.AddProperties(luisResult, recognizerResult);
if (_includeApiResults)
{
recognizerResult.Properties.Add("luisResult", luisResult);
}
+ // Log telemetry
+ await OnRecognizerResultAsync(recognizerResult, turnContext, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false);
+
var traceInfo = JObject.FromObject(
new
{
diff --git a/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisTelemetryConstants.cs b/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisTelemetryConstants.cs
new file mode 100644
index 0000000000..e878856c56
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisTelemetryConstants.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Builder.AI.Luis
+{
+ ///
+ /// The IBotTelemetryClient event and property names that logged by default.
+ ///
+ 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";
+ }
+}
diff --git a/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisUtil.cs b/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisUtil.cs
new file mode 100644
index 0000000000..28fa01b883
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder.AI.LUIS/LuisUtil.cs
@@ -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 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()
+ {
+ {
+ NormalizedIntent(luisResult.TopScoringIntent.Intent),
+ new IntentScore() { Score = luisResult.TopScoringIntent.Score ?? 0 }
+ },
+ };
+ }
+ }
+
+ internal static JObject ExtractEntitiesAndMetadata(IList entities, IList compositeEntities, bool verbose)
+ {
+ var entitiesAndMetadata = new JObject();
+ if (verbose)
+ {
+ entitiesAndMetadata[_metadataKey] = new JObject();
+ }
+
+ var compositeEntityTypes = new HashSet();
+
+ // 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(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)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 PopulateCompositeEntityModel(CompositeEntityModel compositeEntity, IList 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();
+ 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;
+
+ ///
+ /// If a property doesn't exist add it to a new array, otherwise append it to the existing array.
+ ///
+ internal static void AddProperty(JObject obj, string key, JToken value)
+ {
+ if (((IDictionary)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)));
+ }
+ }
+ }
+}
diff --git a/libraries/Microsoft.Bot.Builder/TelemetryConstants.cs b/libraries/Microsoft.Bot.Builder/TelemetryConstants.cs
new file mode 100644
index 0000000000..5578657bfb
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder/TelemetryConstants.cs
@@ -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";
+ }
+}
diff --git a/libraries/Microsoft.Bot.Builder/TelemetryLoggerConstants.cs b/libraries/Microsoft.Bot.Builder/TelemetryLoggerConstants.cs
new file mode 100644
index 0000000000..4b489fc9b9
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder/TelemetryLoggerConstants.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Builder
+{
+ ///
+ /// The Telemetry Logger Event names.
+ ///
+ public static class TelemetryLoggerConstants
+ {
+ // The name of the event when when new message is received from the user.
+ public const string BotMsgReceiveEvent = "BotMessageReceived";
+
+ // The name of the event when logged when a message is sent from the bot to the user.
+ public const string BotMsgSendEvent = "BotMessageSend";
+
+ // The name of the event when a message is updated by the bot.
+ public const string BotMsgUpdateEvent = "BotMessageUpdate";
+
+ // The name of the event when a message is deleted by the bot.
+ public const string BotMsgDeleteEvent = "BotMessageDelete";
+ }
+}
diff --git a/libraries/Microsoft.Bot.Builder/TelemetryLoggerMiddleware.cs b/libraries/Microsoft.Bot.Builder/TelemetryLoggerMiddleware.cs
new file mode 100644
index 0000000000..43b04a2409
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder/TelemetryLoggerMiddleware.cs
@@ -0,0 +1,345 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.Schema;
+
+namespace Microsoft.Bot.Builder
+{
+ ///
+ /// Middleware for logging incoming, outgoing, updated or deleted Activity messages using IBotTelemetryClient.
+ ///
+ public class TelemetryLoggerMiddleware : IMiddleware
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The IBotTelemetryClient implementation used for registering telemetry events.
+ /// (Optional) TRUE to include personally indentifiable information.
+ public TelemetryLoggerMiddleware(IBotTelemetryClient telemetryClient, bool logPersonalInformation = false)
+ {
+ TelemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient));
+ LogPersonalInformation = logPersonalInformation;
+ }
+
+ ///
+ /// Gets a value indicating whether indicates whether to log personal information into events.
+ /// By default, the following properties will not be logged if this is set to false:
+ /// Activity.From.Name
+ /// Activity.Text
+ /// Activity.Speak
+ ///
+ ///
+ /// A value indicating whether indicates whether to log personal information into events.
+ ///
+ public bool LogPersonalInformation { get; }
+
+ ///
+ /// Gets the IBotTelemetryClient.
+ ///
+ ///
+ /// The IBotTelemetryClient.
+ ///
+ public IBotTelemetryClient TelemetryClient { get; }
+
+ ///
+ /// Invoked when a message is received from the user.
+ /// Performs logging of telemetry data using the IBotTelemetryClient.TrackEvent() method.
+ /// This event name used is "BotMessageReceived".
+ ///
+ /// Current activity sent from user.
+ /// cancellation token that can be used by other objects
+ /// or threads to receive notice of cancellation.
+ /// A task that represents the work queued to execute.
+ public Task OnReceiveActivityAsync(Activity activity, CancellationToken cancellation)
+ {
+ TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgReceiveEvent, FillReceiveEventProperties(activity));
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Invoked when the bot sends a message to the user.
+ /// Performs logging of telemetry data using the IBotTelemetryClient.TrackEvent() method.
+ /// This event name used is "BotMessageSend".
+ ///
+ /// Current activity sent from user.
+ /// cancellation token that can be used by other objects
+ /// or threads to receive notice of cancellation.
+ /// A task that represents the work queued to execute.
+ public Task OnSendActivityAsync(Activity activity, CancellationToken cancellation)
+ {
+ TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgSendEvent, FillSendEvendProperties(activity));
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Invoked when the bot updates a message.
+ /// Performs logging of telemetry data using the IBotTelemetryClient.TrackEvent() method.
+ /// This event name used is "BotMessageUpdate".
+ ///
+ /// Current activity sent from user.
+ /// cancellation token that can be used by other objects
+ /// or threads to receive notice of cancellation.
+ /// A task that represents the work queued to execute.
+ public Task OnUpdateActivityAsync(Activity activity, CancellationToken cancellation)
+ {
+ TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgUpdateEvent, FillUpdateEventProperties(activity));
+
+ return Task.CompletedTask;
+
+ }
+
+ ///
+ /// Invoked when the bot deletes a message.
+ /// Performs logging of telemetry data using the IBotTelemetryClient.TrackEvent() method.
+ /// This event name used is "BotMessageDelete".
+ ///
+ /// Current activity sent from user.
+ /// cancellation token that can be used by other objects
+ /// or threads to receive notice of cancellation.
+ /// A task that represents the work queued to execute.
+ public Task OnDeleteActivityAsync(Activity activity, CancellationToken cancellation)
+ {
+ TelemetryClient.TrackEvent(TelemetryLoggerConstants.BotMsgDeleteEvent, FillDeleteEventProperties(activity));
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Logs events based on incoming and outgoing activities using the IBotTelemetryClient interface.
+ ///
+ /// The context object for this turn.
+ /// The delegate to call to continue the bot middleware pipeline.
+ /// A cancellation token that can be used by other objects
+ /// or threads to receive notice of cancellation.
+ /// A task that represents the work queued to execute.
+ ///
+ ///
+ public async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
+ {
+ BotAssert.ContextNotNull(context);
+
+ // log incoming activity at beginning of turn
+ if (context.Activity != null)
+ {
+ var activity = context.Activity;
+
+ // Log Bot Message Received
+ await OnReceiveActivityAsync(activity, cancellationToken).ConfigureAwait(false);
+ }
+
+ // hook up onSend pipeline
+ context.OnSendActivities(async (ctx, activities, nextSend) =>
+ {
+ // run full pipeline
+ var responses = await nextSend().ConfigureAwait(false);
+
+ foreach (var activity in activities)
+ {
+ await OnSendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
+ }
+
+ return responses;
+ });
+
+ // hook up update activity pipeline
+ context.OnUpdateActivity(async (ctx, activity, nextUpdate) =>
+ {
+ // run full pipeline
+ var response = await nextUpdate().ConfigureAwait(false);
+
+ await OnUpdateActivityAsync(activity, cancellationToken).ConfigureAwait(false);
+
+ return response;
+ });
+
+ // hook up delete activity pipeline
+ context.OnDeleteActivity(async (ctx, reference, nextDelete) =>
+ {
+ // run full pipeline
+ await nextDelete().ConfigureAwait(false);
+
+ var deleteActivity = new Activity
+ {
+ Type = ActivityTypes.MessageDelete,
+ Id = reference.ActivityId,
+ }
+ .ApplyConversationReference(reference, isIncoming: false)
+ .AsMessageDeleteActivity();
+
+ await OnDeleteActivityAsync((Activity)deleteActivity, cancellationToken).ConfigureAwait(false);
+ });
+
+ if (nextTurn != null)
+ {
+ await nextTurn(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Fills the Event properties for BotMessageReceived.
+ /// These properties are logged in the IBotTelemetryClient.TrackEvent method when a message is received from the user.
+ ///
+ /// Last activity sent from user.
+ /// Additional properties to add to the event.
+ /// A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageReceived event.
+ protected Dictionary FillReceiveEventProperties(Activity activity, Dictionary additionalProperties = null)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.FromIdProperty, activity.From.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ { TelemetryConstants.LocaleProperty, activity.Locale },
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.RecipientNameProperty, activity.Recipient.Name },
+ };
+
+ // Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples
+ if (LogPersonalInformation)
+ {
+ if (!string.IsNullOrWhiteSpace(activity.From.Name))
+ {
+ properties.Add(TelemetryConstants.FromNameProperty, activity.From.Name);
+ }
+
+ if (!string.IsNullOrWhiteSpace(activity.Text))
+ {
+ properties.Add(TelemetryConstants.TextProperty, activity.Text);
+ }
+
+ if (!string.IsNullOrWhiteSpace(activity.Speak))
+ {
+ properties.Add(TelemetryConstants.SpeakProperty, activity.Speak);
+ }
+ }
+
+ // Additional Properties can override "stock" properties.
+ if (additionalProperties != null)
+ {
+ return additionalProperties.Concat(properties)
+ .GroupBy(kv => kv.Key)
+ .ToDictionary(g => g.Key, g => g.First().Value);
+ }
+
+ return properties;
+ }
+
+ ///
+ /// Fills the event properties for BotMessageSend.
+ /// These properties are logged when an activity message is sent by the Bot to the user.
+ ///
+ /// Last activity sent from user.
+ /// Additional properties to add to the event.
+ /// A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageSend event.
+ protected Dictionary FillSendEvendProperties(Activity activity, Dictionary additionalProperties = null)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.ReplyActivityIDProperty, activity.ReplyToId },
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ { TelemetryConstants.LocaleProperty, activity.Locale },
+ };
+
+ // Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples
+ if (LogPersonalInformation)
+ {
+ if (!string.IsNullOrWhiteSpace(activity.Recipient.Name))
+ {
+ properties.Add(TelemetryConstants.RecipientNameProperty, activity.Recipient.Name);
+ }
+
+ if (!string.IsNullOrWhiteSpace(activity.Text))
+ {
+ properties.Add(TelemetryConstants.TextProperty, activity.Text);
+ }
+
+ if (!string.IsNullOrWhiteSpace(activity.Speak))
+ {
+ properties.Add(TelemetryConstants.SpeakProperty, activity.Speak);
+ }
+ }
+
+ // Additional Properties can override "stock" properties.
+ if (additionalProperties != null)
+ {
+ return additionalProperties.Concat(properties)
+ .GroupBy(kv => kv.Key)
+ .ToDictionary(g => g.Key, g => g.First().Value);
+ }
+
+ return properties;
+ }
+
+ ///
+ /// Fills the event properties for BotMessageUpdate.
+ /// These properties are logged when an activity message is updated by the Bot.
+ /// For example, if a card is interacted with by the use, and the card needs to be updated to reflect
+ /// some interaction.
+ ///
+ /// Last activity sent from user.
+ /// Additional properties to add to the event.
+ /// A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageUpdate event.
+ protected Dictionary FillUpdateEventProperties(Activity activity, Dictionary additionalProperties = null)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.ConversationIdProperty, activity.Conversation.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ { TelemetryConstants.LocaleProperty, activity.Locale },
+ };
+
+ // Use the LogPersonalInformation flag to toggle logging PII data, text is a common example
+ if (LogPersonalInformation && !string.IsNullOrWhiteSpace(activity.Text))
+ {
+ properties.Add(TelemetryConstants.TextProperty, activity.Text);
+ }
+
+ // Additional Properties can override "stock" properties.
+ if (additionalProperties != null)
+ {
+ return additionalProperties.Concat(properties)
+ .GroupBy(kv => kv.Key)
+ .ToDictionary(g => g.Key, g => g.First().Value);
+ }
+
+ return properties;
+
+ return properties;
+ }
+
+ ///
+ /// Fills the event properties for BotMessageDelete.
+ /// These properties are logged when an activity message is deleted by the Bot.
+ ///
+ /// The Activity object deleted by bot.
+ /// Additional properties to add to the event.
+ /// A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageDelete event.
+ protected Dictionary FillDeleteEventProperties(IMessageDeleteActivity activity, Dictionary additionalProperties = null)
+ {
+ var properties = new Dictionary()
+ {
+ { TelemetryConstants.RecipientIdProperty, activity.Recipient.Id },
+ { TelemetryConstants.ConversationIdProperty, activity.Conversation.Id },
+ { TelemetryConstants.ConversationNameProperty, activity.Conversation.Name },
+ };
+
+ // Additional Properties can override "stock" properties.
+ if (additionalProperties != null)
+ {
+ return additionalProperties.Concat(properties)
+ .GroupBy(kv => kv.Key)
+ .ToDictionary(g => g.Key, g => g.First().Value);
+ }
+
+ return properties;
+ }
+ }
+}
diff --git a/tests/Microsoft.Bot.Builder.AI.LUIS.Tests/LuisRecognizerTests.cs b/tests/Microsoft.Bot.Builder.AI.LUIS.Tests/LuisRecognizerTests.cs
index a6e1080357..9c50ada62d 100644
--- a/tests/Microsoft.Bot.Builder.AI.LUIS.Tests/LuisRecognizerTests.cs
+++ b/tests/Microsoft.Bot.Builder.AI.LUIS.Tests/LuisRecognizerTests.cs
@@ -14,6 +14,7 @@
using Microsoft.Bot.Configuration;
using Microsoft.Bot.Schema;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RichardSzalay.MockHttp;
@@ -514,6 +515,308 @@ public void UserAgentContainsProductVersion()
Assert.IsTrue(userAgent.Contains("Microsoft.Bot.Builder.AI.Luis/4"));
}
+ [TestMethod]
+ public void Telemetry_Construction()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var fieldInfo = typeof(LuisRecognizer).GetField("_application", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ // Act
+ var recognizer = new LuisRecognizer(endpoint);
+
+ // Assert
+ var app = (LuisApplication)fieldInfo.GetValue(recognizer);
+ Assert.AreEqual("b31aeaf3-3511-495b-a07f-571fc873214b", app.ApplicationId);
+ Assert.AreEqual("048ec46dc58e495482b0c447cfdbd291", app.EndpointKey);
+ Assert.AreEqual("https://westus.api.cognitive.microsoft.com", app.Endpoint);
+ }
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_OverrideOnLogAsync()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var clientHandler = new EmptyLuisResponseClientHandler();
+ var luisApp = new LuisApplication(endpoint);
+ var telemetryClient = new Mock();
+ var adapter = new NullAdapter();
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = "please book from May 5 to June 6",
+ Recipient = new ChannelAccount(), // to no where
+ From = new ChannelAccount(), // from no one
+ Conversation = new ConversationAccount() // on no conversation
+ };
+
+ var turnContext = new TurnContext(adapter, activity);
+ var recognizer = new LuisRecognizer(luisApp, null, false, clientHandler, telemetryClient.Object);
+
+ // Act
+ var additionalProperties = new Dictionary
+ {
+ { "test", "testvalue" },
+ { "foo", "foovalue" },
+ };
+ var result = await recognizer.RecognizeAsync(turnContext, additionalProperties).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(telemetryClient.Invocations.Count, 1);
+ Assert.AreEqual(telemetryClient.Invocations[0].Arguments[0].ToString(), "LuisResult");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("test"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["test"] == "testvalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("foo"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["foo"] == "foovalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("applicationId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intent"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intentScore"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("fromId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("entities"));
+
+ }
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_OverrideOnDeriveAsync()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var clientHandler = new EmptyLuisResponseClientHandler();
+ var luisApp = new LuisApplication(endpoint);
+ var telemetryClient = new Mock();
+ var adapter = new NullAdapter();
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = "please book from May 5 to June 6",
+ Recipient = new ChannelAccount(), // to no where
+ From = new ChannelAccount(), // from no one
+ Conversation = new ConversationAccount() // on no conversation
+ };
+
+ var turnContext = new TurnContext(adapter, activity);
+ var recognizer = new TelemetryOverrideRecognizer(telemetryClient.Object, luisApp, null, false, false, clientHandler);
+
+ var additionalProperties = new Dictionary
+ {
+ { "test", "testvalue" },
+ { "foo", "foovalue" },
+ };
+ var result = await recognizer.RecognizeAsync(turnContext, additionalProperties).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(telemetryClient.Invocations.Count, 2);
+ Assert.AreEqual(telemetryClient.Invocations[0].Arguments[0].ToString(), "LuisResult");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("MyImportantProperty"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["MyImportantProperty"] == "myImportantValue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("test"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["test"] == "testvalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("foo"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["foo"] == "foovalue");
+ Assert.AreEqual(telemetryClient.Invocations[1].Arguments[0].ToString(), "MySecondEvent");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[1].Arguments[1]).ContainsKey("MyImportantProperty2"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[1].Arguments[1])["MyImportantProperty2"] == "myImportantValue2");
+ }
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_OverrideFillAsync()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var clientHandler = new EmptyLuisResponseClientHandler();
+ var luisApp = new LuisApplication(endpoint);
+ var telemetryClient = new Mock();
+ var adapter = new NullAdapter();
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = "please book from May 5 to June 6",
+ Recipient = new ChannelAccount(), // to no where
+ From = new ChannelAccount(), // from no one
+ Conversation = new ConversationAccount() // on no conversation
+ };
+
+ var turnContext = new TurnContext(adapter, activity);
+ var recognizer = new OverrideFillRecognizer(telemetryClient.Object, luisApp, null, false, false, clientHandler);
+
+ var additionalProperties = new Dictionary
+ {
+ { "test", "testvalue" },
+ { "foo", "foovalue" },
+ };
+ var additionalMetrics = new Dictionary
+ {
+ { "moo", 3.14159 },
+ { "boo", 2.11 },
+ };
+
+ var result = await recognizer.RecognizeAsync(turnContext, additionalProperties, additionalMetrics).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(telemetryClient.Invocations.Count, 2);
+ Assert.AreEqual(telemetryClient.Invocations[0].Arguments[0].ToString(), "LuisResult");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("MyImportantProperty"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["MyImportantProperty"] == "myImportantValue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("test"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["test"] == "testvalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("foo"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["foo"] == "foovalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[2]).ContainsKey("moo"));
+ Assert.AreEqual(((Dictionary)telemetryClient.Invocations[0].Arguments[2])["moo"], 3.14159);
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[2]).ContainsKey("boo"));
+ Assert.AreEqual(((Dictionary)telemetryClient.Invocations[0].Arguments[2])["boo"], 2.11);
+
+ Assert.AreEqual(telemetryClient.Invocations[1].Arguments[0].ToString(), "MySecondEvent");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[1].Arguments[1]).ContainsKey("MyImportantProperty2"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[1].Arguments[1])["MyImportantProperty2"] == "myImportantValue2");
+ }
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_NoOverrideAsync()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var clientHandler = new EmptyLuisResponseClientHandler();
+ var luisApp = new LuisApplication(endpoint);
+ var telemetryClient = new Mock();
+ var adapter = new NullAdapter();
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = "please book from May 5 to June 6",
+ Recipient = new ChannelAccount(), // to no where
+ From = new ChannelAccount(), // from no one
+ Conversation = new ConversationAccount() // on no conversation
+ };
+
+ var turnContext = new TurnContext(adapter, activity);
+ var recognizer = new LuisRecognizer(luisApp, null, false, clientHandler, telemetryClient.Object);
+
+ // Act
+ var result = await recognizer.RecognizeAsync(turnContext, CancellationToken.None).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(telemetryClient.Invocations.Count, 1);
+ Assert.AreEqual(telemetryClient.Invocations[0].Arguments[0].ToString(), "LuisResult");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("applicationId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intent"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intentScore"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("fromId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("entities"));
+ }
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_Convert()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var clientHandler = new EmptyLuisResponseClientHandler();
+ var luisApp = new LuisApplication(endpoint);
+ var telemetryClient = new Mock();
+ var adapter = new NullAdapter();
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = "please book from May 5 to June 6",
+ Recipient = new ChannelAccount(), // to no where
+ From = new ChannelAccount(), // from no one
+ Conversation = new ConversationAccount() // on no conversation
+ };
+
+ var turnContext = new TurnContext(adapter, activity);
+ var recognizer = new LuisRecognizer(luisApp, null, false, clientHandler, telemetryClient.Object);
+
+ // Act
+ // Use a class the converts the Recognizer Result..
+ var result = await recognizer.RecognizeAsync(turnContext, CancellationToken.None).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(telemetryClient.Invocations.Count, 1);
+ Assert.AreEqual(telemetryClient.Invocations[0].Arguments[0].ToString(), "LuisResult");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("applicationId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intent"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intentScore"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("fromId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("entities"));
+ }
+
+
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_ConvertParms()
+ {
+ // Arrange
+ // Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
+ // theses are GUIDs edited to look right to the parsing and validation code.
+ var endpoint = "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360&subscription-key=048ec46dc58e495482b0c447cfdbd291&q=";
+ var clientHandler = new EmptyLuisResponseClientHandler();
+ var luisApp = new LuisApplication(endpoint);
+ var telemetryClient = new Mock();
+ var adapter = new NullAdapter();
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = "please book from May 5 to June 6",
+ Recipient = new ChannelAccount(), // to no where
+ From = new ChannelAccount(), // from no one
+ Conversation = new ConversationAccount() // on no conversation
+ };
+
+ var turnContext = new TurnContext(adapter, activity);
+ var recognizer = new LuisRecognizer(luisApp, null, false, clientHandler, telemetryClient.Object);
+
+ // Act
+ var additionalProperties = new Dictionary
+ {
+ { "test", "testvalue" },
+ { "foo", "foovalue" },
+ };
+ var additionalMetrics = new Dictionary
+ {
+ { "moo", 3.14159 },
+ { "luis", 1.0001 },
+ };
+
+ var result = await recognizer.RecognizeAsync(turnContext, additionalProperties, additionalMetrics, CancellationToken.None).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(telemetryClient.Invocations.Count, 1);
+ Assert.AreEqual(telemetryClient.Invocations[0].Arguments[0].ToString(), "LuisResult");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("test"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["test"] == "testvalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("foo"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1])["foo"] == "foovalue");
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("applicationId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intent"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("intentScore"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("fromId"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[1]).ContainsKey("entities"));
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[2]).ContainsKey("moo"));
+ Assert.AreEqual(((Dictionary)telemetryClient.Invocations[0].Arguments[2])["moo"], 3.14159);
+ Assert.IsTrue(((Dictionary)telemetryClient.Invocations[0].Arguments[2]).ContainsKey("luis"));
+ Assert.AreEqual(((Dictionary)telemetryClient.Invocations[0].Arguments[2])["luis"], 1.0001);
+ }
+
+
+
// 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)
{
@@ -650,6 +953,79 @@ private string GetFilePath(string fileName)
}
}
+ public class TelemetryOverrideRecognizer : LuisRecognizer
+ {
+ public TelemetryOverrideRecognizer(IBotTelemetryClient telemetryClient, LuisApplication application, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, bool logPersonalInformation = false, HttpClientHandler clientHandler = null)
+ : base(application, predictionOptions, includeApiResults, clientHandler, telemetryClient)
+ {
+ LogPersonalInformation = logPersonalInformation;
+ }
+
+ override protected Task OnRecognizerResultAsync(RecognizerResult recognizerResult, ITurnContext turnContext, Dictionary properties = null, Dictionary metrics = null, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ properties.TryAdd("MyImportantProperty", "myImportantValue");
+ // Log event
+ TelemetryClient.TrackEvent(
+ LuisTelemetryConstants.LuisResult,
+ properties,
+ metrics);
+ // Create second event.
+ var secondEventProperties = new Dictionary();
+ secondEventProperties.Add("MyImportantProperty2",
+ "myImportantValue2");
+ TelemetryClient.TrackEvent(
+ "MySecondEvent",
+ secondEventProperties);
+ return Task.CompletedTask;
+ }
+ }
+
+ public class OverrideFillRecognizer : LuisRecognizer
+ {
+ public OverrideFillRecognizer(IBotTelemetryClient telemetryClient, LuisApplication application, LuisPredictionOptions predictionOptions = null, bool includeApiResults = false, bool logPersonalInformation = false, HttpClientHandler clientHandler = null)
+ : base(application, predictionOptions, includeApiResults, clientHandler, telemetryClient)
+ {
+ LogPersonalInformation = logPersonalInformation;
+ }
+
+ override protected async Task OnRecognizerResultAsync(RecognizerResult recognizerResult, ITurnContext turnContext, Dictionary telemetryProperties = null, Dictionary telemetryMetrics = null, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var properties = await FillLuisEventPropertiesAsync(recognizerResult, turnContext, telemetryProperties, cancellationToken).ConfigureAwait(false);
+
+ properties.TryAdd("MyImportantProperty", "myImportantValue");
+ // Log event
+ TelemetryClient.TrackEvent(
+ LuisTelemetryConstants.LuisResult,
+ properties,
+ telemetryMetrics);
+
+ // Create second event.
+ var secondEventProperties = new Dictionary();
+ secondEventProperties.Add("MyImportantProperty2",
+ "myImportantValue2");
+ TelemetryClient.TrackEvent(
+ "MySecondEvent",
+ secondEventProperties);
+ }
+ }
+
+ public class TelemetryConvertResult : IRecognizerConvert
+ {
+ RecognizerResult _result;
+ public TelemetryConvertResult()
+ {
+ }
+
+ ///
+ /// Convert recognizer result.
+ ///
+ /// Result to convert.
+ public void Convert(dynamic result)
+ {
+ _result = result as RecognizerResult;
+ }
+ }
+
public class MockedHttpClientHandler : HttpClientHandler
{
private readonly HttpClient client;
diff --git a/tests/Microsoft.Bot.Builder.Tests/TelemetryMiddlewareTests.cs b/tests/Microsoft.Bot.Builder.Tests/TelemetryMiddlewareTests.cs
new file mode 100644
index 0000000000..c82685ab2f
--- /dev/null
+++ b/tests/Microsoft.Bot.Builder.Tests/TelemetryMiddlewareTests.cs
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Bot.Builder.Adapters;
+using Microsoft.Bot.Schema;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Builder.Tests
+{
+ [TestClass]
+ public class TelemetryMiddlewareTests
+ {
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Telemetry_LogActivities()
+ {
+ var mockTelemetryClient = new Mock();
+ TestAdapter adapter = new TestAdapter()
+ .Use(new TelemetryLoggerMiddleware(mockTelemetryClient.Object, logPersonalInformation: true));
+ string conversationId = null;
+
+ await new TestFlow(adapter, async (context, cancellationToken) =>
+ {
+ conversationId = context.Activity.Conversation.Id;
+ var typingActivity = new Activity
+ {
+ Type = ActivityTypes.Typing,
+ RelatesTo = context.Activity.RelatesTo
+ };
+ await context.SendActivityAsync(typingActivity);
+ await Task.Delay(500);
+ await context.SendActivityAsync("echo:" + context.Activity.Text);
+ })
+ .Send("foo")
+ .AssertReply((activity) => Assert.AreEqual(activity.Type, ActivityTypes.Typing))
+ .AssertReply("echo:foo")
+ .Send("bar")
+ .AssertReply((activity) => Assert.AreEqual(activity.Type, ActivityTypes.Typing))
+ .AssertReply("echo:bar")
+ .StartTestAsync();
+
+ Assert.AreEqual(mockTelemetryClient.Invocations.Count, 6);
+ Assert.AreEqual(mockTelemetryClient.Invocations[0].Arguments[0], "BotMessageReceived"); // Check initial message
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).Count == 7);
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("fromId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("conversationName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("locale"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("recipientId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("recipientName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("fromName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1]).ContainsKey("text"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[0].Arguments[1])["text"] == "foo");
+
+ Assert.AreEqual(mockTelemetryClient.Invocations[1].Arguments[0], "BotMessageSend"); // Check Typing message
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[1].Arguments[1]).Count == 5);
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[1].Arguments[1]).ContainsKey("replyActivityId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[1].Arguments[1]).ContainsKey("recipientId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[1].Arguments[1]).ContainsKey("conversationName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[1].Arguments[1]).ContainsKey("locale"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[1].Arguments[1]).ContainsKey("recipientName"));
+
+ Assert.AreEqual(mockTelemetryClient.Invocations[2].Arguments[0], "BotMessageSend"); // Check message reply
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).Count == 6);
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).ContainsKey("replyActivityId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).ContainsKey("recipientId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).ContainsKey("conversationName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).ContainsKey("locale"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).ContainsKey("recipientName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1]).ContainsKey("text"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[2].Arguments[1])["text"] == "echo:foo");
+
+ Assert.AreEqual(mockTelemetryClient.Invocations[3].Arguments[0], "BotMessageReceived"); // Check bar message
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).Count == 7);
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("fromId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("conversationName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("locale"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("recipientId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("recipientName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("fromName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("text"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1])["text"] == "bar");
+ }
+
+ [TestMethod]
+ [TestCategory("Telemetry")]
+ public async Task Transcript_LogUpdateActivities()
+ {
+ var mockTelemetryClient = new Mock();
+ TestAdapter adapter = new TestAdapter()
+ .Use(new TelemetryLoggerMiddleware(mockTelemetryClient.Object, logPersonalInformation: true));
+ string conversationId = null;
+ Activity activityToUpdate = null;
+ await new TestFlow(adapter, async (context, cancellationToken) =>
+ {
+ conversationId = context.Activity.Conversation.Id;
+ if (context.Activity.Text == "update")
+ {
+ activityToUpdate.Text = "new response";
+ await context.UpdateActivityAsync(activityToUpdate);
+ }
+ else
+ {
+ var activity = context.Activity.CreateReply("response");
+ var response = await context.SendActivityAsync(activity);
+ activity.Id = response.Id;
+
+ // clone the activity, so we can use it to do an update
+ activityToUpdate = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(activity));
+ }
+ })
+ .Send("foo")
+ .Send("update")
+ .AssertReply("new response")
+ .StartTestAsync();
+
+ Assert.AreEqual(mockTelemetryClient.Invocations.Count, 4);
+ Assert.AreEqual(mockTelemetryClient.Invocations[3].Arguments[0], "BotMessageUpdate"); // Check update message
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).Count == 5);
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("recipientId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("conversationId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("conversationName"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("locale"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("text"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1])["text"] == "new response");
+ }
+
+
+ [TestMethod]
+ [TestCategory("Middleware")]
+ public async Task Transcript_LogDeleteActivities()
+ {
+ var mockTelemetryClient = new Mock();
+ TestAdapter adapter = new TestAdapter()
+ .Use(new TelemetryLoggerMiddleware(mockTelemetryClient.Object, logPersonalInformation: true));
+ string conversationId = null;
+ string activityId = null;
+ await new TestFlow(adapter, async (context, cancellationToken) =>
+ {
+ conversationId = context.Activity.Conversation.Id;
+ if (context.Activity.Text == "deleteIt")
+ {
+ await context.DeleteActivityAsync(activityId);
+ }
+ else
+ {
+ var activity = context.Activity.CreateReply("response");
+ var response = await context.SendActivityAsync(activity);
+ activityId = response.Id;
+ }
+ })
+ .Send("foo")
+ .AssertReply("response")
+ .Send("deleteIt")
+ .StartTestAsync();
+ Assert.AreEqual(mockTelemetryClient.Invocations.Count, 4);
+ Assert.AreEqual(mockTelemetryClient.Invocations[3].Arguments[0], "BotMessageDelete"); // Check delete message
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).Count == 3);
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("recipientId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("conversationId"));
+ Assert.IsTrue(((Dictionary)mockTelemetryClient.Invocations[3].Arguments[1]).ContainsKey("conversationName"));
+ }
+ }
+}