diff --git a/packages/Microsoft.Bot.Components.sln b/packages/Microsoft.Bot.Components.sln index a4a0dd6f3..72d47936c 100644 --- a/packages/Microsoft.Bot.Components.sln +++ b/packages/Microsoft.Bot.Components.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Recognizers.CLURecognizer", "Recognizers\ConversationLanguageUnderstanding\dotnet\Microsoft.Bot.Components.Recognizers.CLURecognizer.csproj", "{5432DA4A-3A85-4FFD-9593-F1E55F97B4F6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Recognizers.OrchestrationRecognizer", "Recognizers\OrchestrationWorkflow\dotnet\Microsoft.Bot.Components.Recognizers.OrchestrationRecognizer.csproj", "{E2CD0D37-B533-490C-9AF6-D175B02CA2AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {5432DA4A-3A85-4FFD-9593-F1E55F97B4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU {5432DA4A-3A85-4FFD-9593-F1E55F97B4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {5432DA4A-3A85-4FFD-9593-F1E55F97B4F6}.Release|Any CPU.Build.0 = Release|Any CPU + {E2CD0D37-B533-490C-9AF6-D175B02CA2AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2CD0D37-B533-490C-9AF6-D175B02CA2AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2CD0D37-B533-490C-9AF6-D175B02CA2AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2CD0D37-B533-490C-9AF6-D175B02CA2AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CLU/CluAdaptiveRecognizer.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CLU/CluAdaptiveRecognizer.cs new file mode 100644 index 000000000..61da8edb8 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CLU/CluAdaptiveRecognizer.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveExpressions.Properties; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Components.Recognizers.CLURecognizer; +using Microsoft.Bot.Components.Recognizers.CLURecognizer.CLU; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Recognizers +{ + public class CluAdaptiveRecognizer : Recognizer + { + /// + /// The declarative type for this recognizer. + /// + [JsonProperty("$kind")] + public const string Kind = "Microsoft.OrchestrationRecognizer"; + + /// + /// Gets or sets the ProjectName of your Conversation Language Understanding service. + /// + /// + /// The project name of your Conversation Language Understanding service. + /// + [JsonProperty("projectName")] + public StringExpression ProjectName { get; set; } = string.Empty; + + /// + /// Gets or sets the Endpoint for your Conversation Language Understanding service. + /// + /// + /// The endpoint of your Conversation Language Understanding service. + /// + [JsonProperty("endpoint")] + public StringExpression Endpoint { get; set; } = string.Empty; + + /// + /// Gets or sets the EndpointKey for your Conversation Language Understanding service. + /// + /// + /// The endpoint key for your Conversation Language Understanding service. + /// + [JsonProperty("endpointKey")] + public StringExpression EndpointKey { get; set; } = string.Empty; + + /// + /// Gets or sets the DeploymentName for your Conversation Language Understanding service. + /// + /// + /// The deployment name for your Conversation Language Understanding service. + /// + [JsonProperty("deploymentName")] + public StringExpression DeploymentName { get; set; } = string.Empty; + + /// + /// Gets or sets the flag to determine if personal information should be logged in telemetry. + /// + /// + /// The flag to indicate in personal information should be logged in telemetry. + /// + [JsonProperty("logPersonalInformation")] + public BoolExpression LogPersonalInformation { get; set; } = "=settings.runtimeSettings.telemetry.logPersonalInformation"; + + /// + /// Gets or sets a value indicating whether API results should be included. + /// + /// True to include API results. + /// This is mainly useful for testing or getting access to CLU features not yet in the SDK. + [JsonProperty("includeAPIResults")] + public BoolExpression IncludeAPIResults { get; set; } = false; + + /// + /// Gets or sets a value indicating the string index type to include in the the CLU request body. + /// + /// + /// A value indicating the string index type to include in the the CLU request body. + /// + [JsonProperty("cluRequestBodyStringIndexType")] + public StringExpression CluRequestBodyStringIndexType { get; set; } = CluConstants.RequestOptions.StringIndexType; + + /// + /// Gets or sets a value indicating the CLU API version to use. + /// This can be helpful combined with the flag to get access to CLU features not yet in the SDK. + /// + /// + /// A value indicating the CLU API version to use. + /// + [JsonProperty("cluApiVersion")] + public StringExpression CluApiVersion { get; set; } = CluConstants.RequestOptions.ApiVersion; + + /// + public override async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CancellationToken cancellationToken = default, Dictionary? telemetryProperties = null, Dictionary? telemetryMetrics = null) + { + var recognizer = new CluMainRecognizer(RecognizerOptions(dialogContext), new DefaultHttpClientFactory()); + + var result = await recognizer.RecognizeAsync(dialogContext, activity, cancellationToken).ConfigureAwait(false); + + TrackRecognizerResult(dialogContext, CluConstants.TrackEventOptions.RecognizerResultEventName, FillRecognizerResultTelemetryProperties(result, telemetryProperties, dialogContext), telemetryMetrics); + + return result; + } + + /// + /// Construct recognizer options from the current dialog context. + /// + /// Context. + /// CLU Recognizer options. + public CluRecognizerOptions RecognizerOptions(DialogContext dialogContext) + { + var application = new CluApplication(ProjectName.GetValue(dialogContext.State), EndpointKey.GetValue(dialogContext.State), Endpoint.GetValue(dialogContext.State), DeploymentName.GetValue(dialogContext.State)); + + return new CluRecognizerOptions(application) + { + TelemetryClient = TelemetryClient, + LogPersonalInformation = LogPersonalInformation.GetValue(dialogContext.State), + IncludeAPIResults = IncludeAPIResults.GetValue(dialogContext.State), + CluRequestBodyStringIndexType = CluRequestBodyStringIndexType.GetValue(dialogContext.State), + CluApiVersion = CluApiVersion.GetValue(dialogContext.State), + LogicalHttpClientName = CluConstants.HttpClientOptions.DefaultLogicalName, + }; + } + + /// + /// Uses the returned from the and populates a dictionary of string + /// with properties to be logged into telemetry. Including any additional properties that were passed into the method. + /// + /// An instance of to extract the telemetry properties from. + /// A collection of additional properties to be added to the returned dictionary of properties. + /// An instance of . + /// The dictionary of properties to be logged with telemetry for the recongizer result. + protected override Dictionary FillRecognizerResultTelemetryProperties(RecognizerResult recognizerResult, Dictionary? telemetryProperties, DialogContext dc) + { + var (logPersonalInfoResult, logPersonalInfoError) = LogPersonalInformation.TryGetValue(dc.State); + var (projectNameResult, projectNameError) = ProjectName.TryGetValue(dc.State); + + var topTwoIntents = (recognizerResult.Intents.Count > 0) ? recognizerResult.Intents.OrderByDescending(x => x.Value.Score).Take(2).ToArray() : null; + + // Add the intent score and conversation id properties + var properties = new Dictionary() + { + { CluConstants.Telemetry.ProjectNameProperty, projectNameResult }, + { CluConstants.Telemetry.IntentProperty, topTwoIntents?[0].Key ?? string.Empty }, + { CluConstants.Telemetry.IntentScoreProperty, topTwoIntents?[0].Value.Score?.ToString("N2", CultureInfo.InvariantCulture) ?? "0.00" }, + { CluConstants.Telemetry.Intent2Property, (topTwoIntents?.Length > 1) ? topTwoIntents?[1].Key ?? string.Empty : string.Empty }, + { CluConstants.Telemetry.IntentScore2Property, (topTwoIntents?.Length > 1) ? topTwoIntents?[1].Value.Score?.ToString("N2", CultureInfo.InvariantCulture) ?? "0.00" : "0.00" }, + { CluConstants.Telemetry.FromIdProperty, dc.Context.Activity.From.Id }, + }; + + var entities = recognizerResult.Entities?.ToString(); + + if (!string.IsNullOrWhiteSpace(entities)) + { + properties.Add(CluConstants.Telemetry.EntitiesProperty, entities!); + } + + // Use the LogPersonalInformation flag to toggle logging PII data, text is a common example + if (logPersonalInfoResult && !string.IsNullOrEmpty(dc.Context.Activity.Text)) + { + properties.Add(CluConstants.Telemetry.QuestionProperty, dc.Context.Activity.Text); + } + + // Additional Properties can override "stock" properties. + if (telemetryProperties != null) + { + return telemetryProperties + .Concat(properties) + .GroupBy(kv => kv.Key) + .ToDictionary(g => g.Key, g => g.First().Value); + } + + return properties; + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CLU/CluMainRecognizer.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CLU/CluMainRecognizer.cs new file mode 100644 index 000000000..63d389a48 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CLU/CluMainRecognizer.cs @@ -0,0 +1,458 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer.CLU +{ + /// + /// + /// A CLU based implementation of . + /// + public class CluMainRecognizer : ITelemetryRecognizer + { + private readonly CluRecognizerOptionsBase _recognizerOptions; + private readonly IHttpClientFactory _clientFactory; + private readonly string _cacheKey; + + /// + /// Initializes a new instance of the class. + /// + /// The CLU recognizer version options. + /// The HttpClient factory for the CLU API calls. + public CluMainRecognizer(CluRecognizerOptionsBase recognizerOptions, IHttpClientFactory clientFactory) + { + TelemetryClient = recognizerOptions.TelemetryClient; + LogPersonalInformation = recognizerOptions.LogPersonalInformation; + _recognizerOptions = recognizerOptions; + _clientFactory = clientFactory; + _cacheKey = _recognizerOptions.Application.Endpoint + _recognizerOptions.Application.ProjectName; + } + + /// + /// 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 CluResult event. + /// + /// The being used to log events. + [JsonIgnore] + public IBotTelemetryClient TelemetryClient { get; } + + /// + public virtual async Task RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken) + => await RecognizeInternalAsync(turnContext, null, null, null, cancellationToken).ConfigureAwait(false); + + /// + /// Runs an utterance through a recognizer and returns a generic recognizer result. + /// + /// dialogcontext. + /// activity. + /// cancellationtoken. + /// A representing the result of the asynchronous operation. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CancellationToken cancellationToken) + => await RecognizeInternalAsync(dialogContext, activity, null, null, null, cancellationToken).ConfigureAwait(false); + + /// + public virtual async Task RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(turnContext, null, null, null, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Runs an utterance through a recognizer and returns a strongly-typed recognizer result. + /// + /// type of result. + /// dialogContext. + /// activity. + /// cancellationToken. + /// A representing the result of the asynchronous operation. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CancellationToken cancellationToken) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(dialogContext, activity, null, null, null, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// 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 CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + => await RecognizeInternalAsync(turnContext, null, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false); + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// Context object containing information for a single turn of conversation with a user. + /// activity to recognize. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + => await RecognizeInternalAsync(dialogContext, activity, null, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false); + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// The recognition result type. + /// Context object containing information for a single turn of conversation with a user. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(ITurnContext turnContext, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(turnContext, null, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// The recognition result type. + /// Context object containing information for a single turn of conversation with a user. + /// activity to recognize. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(dialogContext, activity, null, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Runs an utterance through a recognizer and returns a generic recognizer result. + /// + /// Turn context. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Cancellation token. + /// Analysis of utterance. + public virtual async Task RecognizeAsync(ITurnContext turnContext, CluRecognizerOptionsBase recognizerOptions, CancellationToken cancellationToken) + { + return await RecognizeInternalAsync(turnContext, recognizerOptions, null, null, cancellationToken).ConfigureAwait(false); + } + + /// + /// Runs an utterance through a recognizer and returns a generic recognizer result. + /// + /// dialog context. + /// activity to recognize. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Cancellation token. + /// Analysis of utterance. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CluRecognizerOptionsBase recognizerOptions, CancellationToken cancellationToken) + { + return await RecognizeInternalAsync(dialogContext, activity, recognizerOptions, null, null, cancellationToken).ConfigureAwait(false); + } + + /// + /// Runs an utterance through a recognizer and returns a strongly-typed recognizer result. + /// + /// The recognition result type. + /// Turn context. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Cancellation token. + /// Analysis of utterance. + public virtual async Task RecognizeAsync(ITurnContext turnContext, CluRecognizerOptionsBase recognizerOptions, CancellationToken cancellationToken) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(turnContext, recognizerOptions, null, null, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Runs an utterance through a recognizer and returns a strongly-typed recognizer result. + /// + /// The recognition result type. + /// dialog context. + /// activity to recognize. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Cancellation token. + /// Analysis of utterance. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CluRecognizerOptionsBase recognizerOptions, CancellationToken cancellationToken) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(dialogContext, activity, recognizerOptions, null, null, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// Context object containing information for a single turn of conversation with a user. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(ITurnContext turnContext, CluRecognizerOptionsBase recognizerOptions, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + { + return await RecognizeInternalAsync(turnContext, recognizerOptions, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false); + } + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// Context object containing information for a single turn of conversation with a user. + /// activity to recognize. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CluRecognizerOptionsBase recognizerOptions, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + { + return await RecognizeInternalAsync(dialogContext, activity, recognizerOptions, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false); + } + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// The recognition result type. + /// Context object containing information for a single turn of conversation with a user. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(ITurnContext turnContext, CluRecognizerOptionsBase recognizerOptions, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(turnContext, recognizerOptions, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// The recognition result type. + /// Context object containing information for a single turn of conversation with a user. + /// activity to recognize. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(DialogContext dialogContext, Activity activity, CluRecognizerOptionsBase recognizerOptions, Dictionary telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken = default) + where T : IRecognizerConvert, new() + { + var result = new T(); + + result.Convert(await RecognizeInternalAsync(dialogContext, activity, recognizerOptions, telemetryProperties, telemetryMetrics, cancellationToken).ConfigureAwait(false)); + + return result; + } + + /// + /// Return results of the analysis (Suggested actions and intents). + /// + /// No telemetry is provided when using this method. + /// utterance to recognize. + /// A instance to be used by the call. + /// This parameter overrides the default passed in the constructor. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The CLU results of the analysis of the current message text in the current turn's context activity. + public virtual async Task RecognizeAsync(string utterance, CluRecognizerOptionsBase? recognizerOptions, CancellationToken cancellationToken = default) + { + recognizerOptions ??= _recognizerOptions; + + return await RecognizeInternalAsync(utterance, recognizerOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Invoked prior to a CluResult being logged. + /// + /// The CLU 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 CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + protected virtual void OnRecognizerResult(RecognizerResult recognizerResult, ITurnContext turnContext, Dictionary? telemetryProperties, Dictionary? telemetryMetrics) + { + // Track the event + _recognizerOptions.TelemetryClient.TrackEvent(CluConstants.Telemetry.CluResult, FillCluEventProperties(recognizerResult, turnContext, telemetryProperties), telemetryMetrics); + } + + /// + /// Fills the event properties for CluResult 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 CluResult event. + /// additionalProperties + /// A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the BotMessageSend event. + protected Dictionary FillCluEventProperties(RecognizerResult recognizerResult, ITurnContext turnContext, Dictionary? telemetryProperties) + { + var topTwoIntents = (recognizerResult.Intents.Count > 0) ? recognizerResult.Intents.OrderByDescending(x => x.Value.Score).Take(2).ToArray() : null; + + // Add the intent score and conversation id properties + var properties = new Dictionary() + { + { CluConstants.Telemetry.ProjectNameProperty, _recognizerOptions.Application.ProjectName }, + { CluConstants.Telemetry.IntentProperty, topTwoIntents?[0].Key ?? string.Empty }, + { CluConstants.Telemetry.IntentScoreProperty, topTwoIntents?[0].Value.Score?.ToString("N2", CultureInfo.InvariantCulture) ?? "0.00" }, + { CluConstants.Telemetry.Intent2Property, (topTwoIntents?.Length > 1) ? topTwoIntents?[1].Key ?? string.Empty : string.Empty }, + { CluConstants.Telemetry.IntentScore2Property, (topTwoIntents?.Length > 1) ? topTwoIntents?[1].Value.Score?.ToString("N2", CultureInfo.InvariantCulture) ?? "0.00" : "0.00" }, + { CluConstants.Telemetry.FromIdProperty, turnContext.Activity.From.Id }, + }; + + var entities = recognizerResult.Entities?.ToString(); + + if (!string.IsNullOrWhiteSpace(entities)) + { + properties.Add(CluConstants.Telemetry.EntitiesProperty, entities!); + } + + // Use the LogPersonalInformation flag to toggle logging PII data, text is a common example + if (LogPersonalInformation && !string.IsNullOrEmpty(turnContext.Activity.Text)) + { + properties.Add(CluConstants.Telemetry.QuestionProperty, turnContext.Activity.Text); + } + + // Additional Properties can override "stock" properties. + if (telemetryProperties != null) + { + return telemetryProperties + .Concat(properties) + .GroupBy(kv => kv.Key) + .ToDictionary(g => g.Key, g => g.First().Value); + } + + return properties; + } + + /// + /// Returns a RecognizerResult object. + /// + /// Dialog turn Context. + /// CluRecognizerOptions implementation to override current properties. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// RecognizerResult object. + private async Task RecognizeInternalAsync(ITurnContext turnContext, CluRecognizerOptionsBase? predictionOptions, Dictionary? telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken) + { + var recognizer = predictionOptions ?? _recognizerOptions; + + var cached = turnContext.TurnState.Get(_cacheKey); + + if (cached == null) + { + var result = await recognizer.RecognizeInternalAsync(turnContext, _clientFactory.CreateClient(recognizer.LogicalHttpClientName), cancellationToken).ConfigureAwait(false); + + OnRecognizerResult(result, turnContext, telemetryProperties, telemetryMetrics); + + turnContext.TurnState.Set(_cacheKey, result); + + _recognizerOptions.TelemetryClient.TrackEvent(CluConstants.TrackEventOptions.ResultCachedEventName, telemetryProperties, telemetryMetrics); + + return result; + } + + _recognizerOptions.TelemetryClient.TrackEvent(CluConstants.TrackEventOptions.ReadFromCachedResultEventName, telemetryProperties, telemetryMetrics); + + return cached; + } + + /// + /// Returns a RecognizerResult object. + /// + /// Dialog turn Context. + /// activity to recognize. + /// CluRecognizerOptions implementation to override current properties. + /// Additional properties to be logged to telemetry with the CluResult event. + /// Additional metrics to be logged to telemetry with the CluResult event. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// RecognizerResult object. + private async Task RecognizeInternalAsync(DialogContext dialogContext, Activity activity, CluRecognizerOptionsBase? predictionOptions, Dictionary? telemetryProperties, Dictionary? telemetryMetrics, CancellationToken cancellationToken) + { + var recognizer = predictionOptions ?? _recognizerOptions; + var turnContext = dialogContext.Context; + var cached = turnContext.TurnState.Get(_cacheKey); + + if (cached == null) + { + var result = await recognizer.RecognizeInternalAsync(dialogContext, activity, _clientFactory.CreateClient(recognizer.LogicalHttpClientName), cancellationToken).ConfigureAwait(false); + + OnRecognizerResult(result, dialogContext.Context, telemetryProperties, telemetryMetrics); + + turnContext.TurnState.Set(_cacheKey, result); + + _recognizerOptions.TelemetryClient.TrackEvent(CluConstants.TrackEventOptions.ResultCachedEventName, telemetryProperties, telemetryMetrics); + + return result; + } + + _recognizerOptions.TelemetryClient.TrackEvent(CluConstants.TrackEventOptions.ReadFromCachedResultEventName, telemetryProperties, telemetryMetrics); + + return cached; + } + + /// + /// Returns a RecognizerResult object. + /// + /// utterance to recognize. + /// CluRecognizerOptions implementation to override current properties. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// RecognizerResult object. + private async Task RecognizeInternalAsync(string utterance, CluRecognizerOptionsBase? predictionOptions, CancellationToken cancellationToken) + { + var recognizer = predictionOptions ?? _recognizerOptions; + + var result = await recognizer.RecognizeInternalAsync(utterance, _clientFactory.CreateClient(recognizer.LogicalHttpClientName), cancellationToken).ConfigureAwait(false); + + return result; + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluApplication.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluApplication.cs new file mode 100644 index 000000000..a89540405 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluApplication.cs @@ -0,0 +1,97 @@ +using System; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + /// + /// Data describing a CLU application. + /// + public class CluApplication + { + /// + /// Initializes a new instance of the class. + /// + /// CLU project name. + /// CLU subscription or endpoint key. + /// CLU endpoint to use. + /// CLU deployment name. + public CluApplication(string projectName, string endpointKey, string endpoint, string deploymentName) + : this((projectName, endpointKey, endpoint, deploymentName)) + { + } + + /// + /// Initializes a new instance of the class. + /// For unit tests only. + /// + protected CluApplication() + { + } + + private CluApplication(ValueTuple props) + { + var (projectName, endpointKey, endpoint, deploymentName) = props; + + if (string.IsNullOrWhiteSpace(projectName)) + { + throw new ArgumentException($"CLU \"{nameof(projectName)}\" parameter cannot be null or empty."); + } + + if (!Guid.TryParse(endpointKey, out var _)) + { + throw new ArgumentException($"\"{endpointKey}\" is not a valid CLU subscription key."); + } + + if (string.IsNullOrWhiteSpace(endpoint)) + { + throw new ArgumentException($"CLU \"{nameof(endpoint)}\" parameter cannot be null or empty."); + } + + if (!Uri.IsWellFormedUriString(endpoint, UriKind.Absolute)) + { + throw new ArgumentException($"\"{endpoint}\" is not a valid CLU endpoint."); + } + + if (string.IsNullOrWhiteSpace(deploymentName)) + { + throw new ArgumentException($"CLU \"{nameof(deploymentName)}\" parameter cannot be null or empty."); + } + + ProjectName = projectName; + EndpointKey = endpointKey; + Endpoint = endpoint; + DeploymentName = deploymentName; + } + + /// + /// Gets or sets CLU project name. + /// + /// + /// CLU project name. + /// + public string ProjectName { get; set; } = default!; + + /// + /// Gets or sets CLU subscription or endpoint key. + /// + /// + /// CLU subscription or endpoint key. + /// + public string EndpointKey { get; set; } = default!; + + /// + /// Gets or sets CLU endpoint. + /// + /// + /// CLU endpoint where application is hosted. + /// + public string Endpoint { get; set; } = default!; + + /// + /// Gets or sets CLU deployment name. + /// + /// + /// CLU deployment name. + /// + public string DeploymentName { get; set; } = default!; + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluConstants.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluConstants.cs new file mode 100644 index 000000000..0e7a09deb --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluConstants.cs @@ -0,0 +1,179 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + /// + /// The CLU Constants. + /// + [SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Constants.")] + public static class CluConstants + { + /// + /// The recognizer result response property name to include the CLU result. + /// + public const string RecognizerResultResponsePropertyName = "cluResult"; + + /// + /// The CLU track event constants. + /// + public static class TrackEventOptions + { + /// + /// The name of the recognizer result event to track. + /// + public const string RecognizerResultEventName = "CluResult"; + + /// + /// The name of the clu result cached event to track. + /// + public const string ResultCachedEventName = "CluResultCached"; + + /// + /// The name of the read from cached clu result event to track. + /// + public const string ReadFromCachedResultEventName = "ReadFromCachedCluResult"; + } + + /// + /// The CLU response constants. + /// + public static class ResponseOptions + { + /// + /// The CLU response result key. + /// + public const string ResultKey = "result"; + + /// + /// The CLU response prediction key. + /// + public const string PredictionKey = "prediction"; + + public const string TopIntentKey = "topIntent"; + } + + /// + /// The CLU trace constants. + /// + public static class TraceOptions + { + /// + /// The name of the CLU trace activity. + /// + public const string ActivityName = "CluRecognizer"; + + /// + /// The value type for a CLU trace activity. + /// + public const string TraceType = "https://www.clu.ai/schemas/trace"; + + /// + /// The context label for a CLU trace activity. + /// + public const string TraceLabel = "Clu Trace"; + } + + /// + /// The CLU HttpClient constants. + /// + public static class HttpClientOptions + { + /// + /// The default the logical name of the HttpClient to create. + /// + public const string DefaultLogicalName = "clu"; + + /// + /// The default time in milliseconds to wait before the request times out. + /// + public const double Timeout = 100000; + } + + /// + /// The IBotTelemetryClient event and property names that are logged by default. + /// + public static class Telemetry + { + /// + /// The Key used when storing a CLU Result in a custom event within telemetry. + /// + public const string CluResult = "CluResult"; + + /// + /// The Key used when storing a CLU Project Name in a custom event within telemetry. + /// + public const string ProjectNameProperty = "projectName"; + + /// + /// The Key used when storing a CLU intent in a custom event within telemetry. + /// + public const string IntentProperty = "intent"; + + /// + /// The Key used when storing a CLU intent score in a custom event within telemetry. + /// + public const string IntentScoreProperty = "intentScore"; + + /// + /// The Key used when storing a CLU intent in a custom event within telemetry. + /// + public const string Intent2Property = "intent2"; + + /// + /// The Key used when storing a CLU intent score in a custom event within telemetry. + /// + public const string IntentScore2Property = "intentScore2"; + + /// + /// The Key used when storing CLU entities in a custom event within telemetry. + /// + public const string EntitiesProperty = "entities"; + + /// + /// The Key used when storing the CLU query in a custom event within telemetry. + /// + public const string QuestionProperty = "question"; + + /// + /// The Key used when storing the FromId in a custom event within telemetry. + /// + public const string FromIdProperty = "fromId"; + } + + /// + /// The CLU request body default constants. + /// + public static class RequestOptions + { + /// + /// The Kind value of the CLU request body. + /// + public const string Kind = "Conversation"; + + /// + /// The Conversation Item Id value of the CLU request body. + /// + public const string ConversationItemId = "1"; + + /// + /// The Conversation Item Participant Id value of the CLU request body. + /// + public const string ConversationItemParticipantId = "1"; + + /// + /// The String Index Type value of the CLU request body. + /// + public const string StringIndexType = "TextElement_V8"; + + /// + /// The API Version of the CLU service. + /// + public const string ApiVersion = "2022-05-01"; + + /// + /// The name of the CLU subscription key header. + /// + public const string SubscriptionKeyHeaderName = "Ocp-Apim-Subscription-Key"; + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluDelegatingHandler.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluDelegatingHandler.cs new file mode 100644 index 000000000..a12905d75 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluDelegatingHandler.cs @@ -0,0 +1,34 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + internal class CluDelegatingHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Bot Builder Package name and version. + var assemblyName = GetType().Assembly.GetName(); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(assemblyName.Name, assemblyName.Version.ToString())); + + // Platform information: OS and language runtime. + var framework = Assembly + .GetEntryAssembly()? + .GetCustomAttribute()? + .FrameworkName ?? RuntimeInformation.FrameworkDescription; + + var comment = $"({Environment.OSVersion.VersionString};{framework})"; + + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(comment)); + + // Forward the call. + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluExtensions.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluExtensions.cs new file mode 100644 index 000000000..25d64a91f --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluExtensions.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Bot.Builder; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + internal static class CluExtensions + { + internal static IDictionary ExtractIntents(this JObject cluResult) + { + var result = new Dictionary(); + + if (cluResult?["intents"] != null && cluResult["intents"] is JArray cluIntents) + { + foreach (var intent in cluIntents) + { + var category = intent["category"] !.ToString(); + var confidenceScore = intent["confidenceScore"] !.ToString(); + + result.Add( + NormalizedValue(category), + new IntentScore + { + Score = confidenceScore == null ? 0.0 : double.Parse(confidenceScore, CultureInfo.CurrentCulture), + }); + } + } + + return result; + } + + internal static JObject ExtractEntities(this JObject cluResult) + { + var result = new JObject(); + + if (cluResult?["entities"] != null && cluResult["entities"] is JArray cluEntities) + { + foreach (var entity in cluEntities) + { + var category = entity["category"] !.ToString(); + + if (result[NormalizedValue(category)] == null) + { + result[NormalizedValue(category)] = new JArray(entity); + continue; + } + + ((JArray)result[NormalizedValue(category)] !).Add(entity); + } + } + + return result; + } + + private static string NormalizedValue(string value) => value.Replace('.', '_').Replace(' ', '_'); + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerBotComponent.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerBotComponent.cs new file mode 100644 index 000000000..586b096f3 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerBotComponent.cs @@ -0,0 +1,15 @@ +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs.Declarative; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Bot.Components.Recognizers +{ + public class CluRecognizerBotComponent : BotComponent + { + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton((sp) => new DeclarativeType(CluAdaptiveRecognizer.Kind)); + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerOptions.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerOptions.cs new file mode 100644 index 000000000..709601656 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerOptions.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.QnA; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.TraceExtensions; +using Microsoft.Bot.Components.Recognizers.Models; +using Microsoft.Bot.Schema; +using Microsoft.Recognizers.Text.NumberWithUnit; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + /// + /// Options for . + /// + public class CluRecognizerOptions : CluRecognizerOptionsBase + { + /// + /// Initializes a new instance of the class. + /// + /// The CLU application to use to recognize text. + public CluRecognizerOptions(CluApplication application) + : base(application) + { + } + + internal override async Task RecognizeInternalAsync(DialogContext dialogContext, Activity activity, HttpClient httpClient, CancellationToken cancellationToken) + { + return await RecognizeAsync(dialogContext.Context, activity?.Text!, httpClient, cancellationToken).ConfigureAwait(false); + } + + internal override async Task RecognizeInternalAsync(ITurnContext turnContext, HttpClient httpClient, CancellationToken cancellationToken) + { + return await RecognizeAsync(turnContext, turnContext?.Activity?.AsMessageActivity()?.Text!, httpClient, cancellationToken).ConfigureAwait(false); + } + + internal override async Task RecognizeInternalAsync(string utterance, HttpClient httpClient, CancellationToken cancellationToken) + { + return await RecognizeAsync(utterance, httpClient, cancellationToken).ConfigureAwait(false); + } + + private async Task RecognizeAsync(ITurnContext turnContext, string utterance, HttpClient httpClient, CancellationToken cancellationToken) + { + RecognizerResult recognizerResult; + JObject? cluResponse = null; + + if (string.IsNullOrWhiteSpace(utterance)) + { + recognizerResult = new RecognizerResult + { + Text = utterance, + }; + } + else + { + cluResponse = await GetCluResponseAsync(utterance, httpClient, cancellationToken).ConfigureAwait(false); + recognizerResult = BuildRecognizerResultFromOrchestrationResponse(cluResponse, utterance); + } + + var traceInfo = JObject.FromObject( + new + { + recognizerResult, + cluModel = new + { + Application.ProjectName, + }, + cluResult = cluResponse, + }); + + await turnContext.TraceActivityAsync(CluConstants.TraceOptions.ActivityName, traceInfo, CluConstants.TraceOptions.TraceType, CluConstants.TraceOptions.TraceLabel, cancellationToken).ConfigureAwait(false); + + return recognizerResult; + } + + private async Task RecognizeAsync(string utterance, HttpClient httpClient, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(utterance)) + { + return new RecognizerResult + { + Text = utterance, + }; + } + else + { + var cluResponse = await GetCluResponseAsync(utterance, httpClient, cancellationToken).ConfigureAwait(false); + + return BuildRecognizerResultFromOrchestrationResponse(cluResponse, utterance); + } + } + + private async Task GetCluResponseAsync(string utterance, HttpClient httpClient, CancellationToken cancellationToken) + { + var uri = BuildUri(); + var content = BuildRequestBody(utterance); + + using var request = new HttpRequestMessage(HttpMethod.Post, uri.Uri); + using var stringContent = new StringContent(content.ToString(), Encoding.UTF8, "application/json"); + request.Content = stringContent; + request.Headers.Add(CluConstants.RequestOptions.SubscriptionKeyHeaderName, Application.EndpointKey); + + var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + return (JObject)JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync().ConfigureAwait(false), new JsonSerializerSettings { MaxDepth = null }) !; + } + + private JObject BuildRequestBody(string utterance) + { + return JObject.FromObject(new + { + kind = CluConstants.RequestOptions.Kind, + analysisInput = new + { + conversationItem = new + { + id = CluConstants.RequestOptions.ConversationItemId, + participantId = CluConstants.RequestOptions.ConversationItemParticipantId, + text = utterance, + }, + }, + parameters = new + { + projectName = Application.ProjectName, + deploymentName = Application.DeploymentName, + stringIndexType = CluRequestBodyStringIndexType, + }, + }); + } + + private RecognizerResult BuildRecognizerResultFromOrchestrationResponse(JObject cluResponse, string utterance) + { + var result = (JObject)cluResponse[CluConstants.ResponseOptions.ResultKey] !; + var prediction = (JObject)result[CluConstants.ResponseOptions.PredictionKey] !; + var topIntentResultKey = (string)prediction[CluConstants.ResponseOptions.TopIntentKey] !; + var topIntent = ObjectPath.GetPathValue(prediction, $"intents.{topIntentResultKey}"); + var topIntentProjectKind = (string)topIntent["targetProjectKind"] !; + + switch (topIntentProjectKind) + { + case "Conversation": + return BuildRecognizerResultFromClu(topIntent, utterance); + case "QuestionAnswering": + return BuildRecognizerResultFromQuestionAnswering(topIntent, utterance); + } + + return new RecognizerResult() + { + Text = utterance, + Intents = new Dictionary() + { + { "None", new IntentScore { Score = 1.0f } } + } + }; + } + + private RecognizerResult BuildRecognizerResultFromClu(JObject cluResponse, string utterance) + { + var result = (JObject)cluResponse[CluConstants.ResponseOptions.ResultKey] !; + var prediction = (JObject)result[CluConstants.ResponseOptions.PredictionKey] !; + var recognizerResult = new RecognizerResult + { + Text = utterance, + AlteredText = utterance, + Intents = prediction.ExtractIntents(), + Entities = prediction.ExtractEntities(), + }; + + if (IncludeAPIResults) + { + recognizerResult.Properties.Add(CluConstants.RecognizerResultResponsePropertyName, cluResponse); + } + + return recognizerResult; + } + + private RecognizerResult BuildRecognizerResultFromQuestionAnswering(JObject cluResponse, string utterance) + { + var recognizerResult = new RecognizerResult + { + Text = utterance, + Intents = new Dictionary(), + }; + + if (string.IsNullOrEmpty(utterance)) + { + recognizerResult.Intents.Add("None", new IntentScore()); + return recognizerResult; + } + + var result = (JObject)cluResponse[QuestionAnsweringConstants.ResponseOptions.ResultKey] !; + KnowledgeBaseAnswer? topAnswer = null; + KnowledgeBaseAnswers? resultAnswers = null; + if (result != null) + { + resultAnswers = JsonConvert.DeserializeObject(result.ToString(), new JsonSerializerSettings { MaxDepth = null }); + + if (resultAnswers != null) + { + foreach (var answer in resultAnswers.Answers) + { + if (topAnswer == null || topAnswer.ConfidenceScore < answer.ConfidenceScore) + { + topAnswer = answer; + } + } + } + } + + if (topAnswer != null) + { + recognizerResult.Intents.Add(QuestionAnsweringConstants.QnAMatchIntent, new IntentScore { Score = topAnswer.ConfidenceScore }); + + var answerArray = new JArray + { + topAnswer.Answer + }; + ObjectPath.SetPathValue(recognizerResult, "entities.answer", answerArray); + + var instance = new JArray(); + var data = JObject.FromObject(topAnswer); + data["startIndex"] = 0; + data["endIndex"] = utterance.Length; + instance.Add(data); + ObjectPath.SetPathValue(recognizerResult, "entities.$instance.answer", instance); + + recognizerResult.Properties["answers"] = resultAnswers?.Answers; + } + else + { + recognizerResult.Intents.Add("None", new IntentScore { Score = 1.0f }); + } + + return recognizerResult; + } + + private UriBuilder BuildUri() + { + var path = new StringBuilder(Application.Endpoint); + + path.Append($"/language/:analyze-conversations?api-version={CluApiVersion}"); + + var uri = new UriBuilder(path.ToString()); + + var query = HttpUtility.ParseQueryString(uri.Query); + + uri.Query = query.ToString(); + + return uri; + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerOptionsBase.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerOptionsBase.cs new file mode 100644 index 000000000..16f2ff260 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/CluRecognizerOptionsBase.cs @@ -0,0 +1,89 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Options; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + /// + /// CLU Recognizer Options. + /// + public abstract class CluRecognizerOptionsBase + { + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + protected CluRecognizerOptionsBase(CluApplication application) + { + Application = application ?? throw new ArgumentNullException(nameof(application)); + } + + /// + /// Gets the CLU application used to recognize text. + /// + /// + /// The CLU application to use to recognize text. + /// + public CluApplication Application { get; private set; } + + /// + /// Gets or sets the time in milliseconds to wait before the request times out. + /// + /// + /// The time in milliseconds to wait before the request times out. Default is 100000 milliseconds. + /// + public double Timeout { get; set; } = CluConstants.HttpClientOptions.Timeout; + + /// + /// Gets or sets the IBotTelemetryClient used to log the CluResult event. + /// + /// + /// The client used to log telemetry events. + /// + public IBotTelemetryClient TelemetryClient { get; set; } = new NullBotTelemetryClient(); + + /// + /// 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 or sets a value indicating whether flag to indicate if full results from the CLU API should be returned with the recognizer result. + /// + /// A value indicating whether full results from the CLU API should be returned with the recognizer result. + public bool IncludeAPIResults { get; set; } + + /// + /// Gets or sets a value indicating the string index type to include in the the CLU request body. + /// + /// A value indicating the string index type to include in the the CLU request body. + public string CluRequestBodyStringIndexType { get; set; } = CluConstants.RequestOptions.StringIndexType; + + /// + /// Gets or sets a value indicating the api version of the CLU service. + /// + /// A value indicating the api version of the CLU service. + public string CluApiVersion { get; set; } = CluConstants.RequestOptions.ApiVersion; + + /// + /// Gets or sets a value indicating the logical name of the client to create. + /// + /// A value indicating the logical name of the client to create. + public string LogicalHttpClientName { get; set; } = Options.DefaultName; + + // Support original ITurnContext + internal abstract Task RecognizeInternalAsync(ITurnContext turnContext, HttpClient httpClient, CancellationToken cancellationToken); + + // Support DialogContext + internal abstract Task RecognizeInternalAsync(DialogContext context, Activity activity, HttpClient httpClient, CancellationToken cancellationToken); + + // Support string utterance + internal abstract Task RecognizeInternalAsync(string utterance, HttpClient httpClient, CancellationToken cancellationToken); + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/DefaultHttpClientFactory.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/DefaultHttpClientFactory.cs new file mode 100644 index 000000000..a521bc7ef --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/DefaultHttpClientFactory.cs @@ -0,0 +1,64 @@ +using System; +using System.Net.Http; +using Microsoft.Rest; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + /// + /// HttpClientFactory that always returns the same HttpClient instance for CLU calls. + /// + internal class DefaultHttpClientFactory : IHttpClientFactory + { + private static readonly HttpClient _httpClient = new HttpClient(CreateHttpHandlerPipeline(CreateRootHandler(), new CluDelegatingHandler()), false) + { + Timeout = TimeSpan.FromMilliseconds(CluConstants.HttpClientOptions.Timeout), + }; + + /// + /// Returns the same default HttpClient instance. + /// + /// Name is not used in this context. This parameter is here, because it is dictated by the interface method declaration. + /// The same HttpClient instance. + public HttpClient CreateClient(string name) + { + return _httpClient; + } + + private static HttpClientHandler CreateRootHandler() => new HttpClientHandler(); + + private static DelegatingHandler CreateHttpHandlerPipeline(HttpClientHandler httpClientHandler, params DelegatingHandler[] handlers) + { + // Now, the RetryAfterDelegatingHandler should be the absolute outermost handler + // because it's extremely lightweight and non-interfering + DelegatingHandler currentHandler = +#pragma warning disable CA2000 // Dispose objects before losing scope (suppressing this warning, for now! we will address this once we implement HttpClientFactory in a future release) + new RetryDelegatingHandler( + new RetryAfterDelegatingHandler + { + InnerHandler = httpClientHandler + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + if (handlers != null) + { + for (var i = handlers.Length - 1; i >= 0; --i) + { + var handler = handlers[i]; + + // Non-delegating handlers are ignored since we always + // have RetryDelegatingHandler as the outer-most handler + while (handler!.InnerHandler is DelegatingHandler) + { + handler = handler.InnerHandler as DelegatingHandler; + } + + handler.InnerHandler = currentHandler; + + currentHandler = handlers[i]; + } + } + + return currentHandler; + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/Microsoft.Bot.Components.Recognizers.OrchestrationRecognizer.csproj b/packages/Recognizers/OrchestrationWorkflow/dotnet/Microsoft.Bot.Components.Recognizers.OrchestrationRecognizer.csproj new file mode 100644 index 000000000..f5d20fa32 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/Microsoft.Bot.Components.Recognizers.OrchestrationRecognizer.csproj @@ -0,0 +1,35 @@ + + + + Library + netstandard2.0 + 8.0 + enable + + + + Microsoft.Bot.Components.Recognizers.OrchestrationRecognizer + 1.0.2 + msbot-component;msbot-recognizer;composer;botframework;botbuilder + Orchestration Workflow Recognizer + This library implements .NET support for Composer with Orchestration Workflow. + This library implements .NET support for Composer with Orchestration Workflow. + https://github.com/Microsoft/botframework-components/tree/main/packages/Recognizers/OrchestrationWorkflow/dotnet + true + ..\..\..\..\build\35MSSharedLib1024.snk + true + + + + + + + + + + + + + + + diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/Models/KnowledgeBaseAnswer.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/Models/KnowledgeBaseAnswer.cs new file mode 100644 index 000000000..cd16b639f --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/Models/KnowledgeBaseAnswer.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Bot.Builder.AI.QnA; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Recognizers.Models +{ + /// + /// Represents an individual result from a knowledge base query. + /// + internal class KnowledgeBaseAnswer + { + /// + /// Gets the list of questions indexed in the QnA Service for the given answer. + /// + /// + /// The list of questions indexed in the QnA Service for the given answer. + /// + [JsonProperty("questions")] + public List Questions { get; } = new List(); + + /// + /// Gets or sets the answer text. + /// + /// + /// The answer text. + /// + [JsonProperty("answer")] + public string? Answer { get; set; } + + /// Gets metadata associated with the answer, useful to categorize or filter question answers. + /// Metadata associated with the answer, useful to categorize or filter question answers. + [JsonProperty(PropertyName = "metadata")] + public Dictionary Metadata { get; } = new Dictionary(); + + /// + /// Gets or sets the answer's score, from 0.0 (least confidence) to + /// 1.0 (greatest confidence). + /// + /// + /// The answer's score, from 0.0 (least confidence) to + /// 1.0 (greatest confidence). + /// + [JsonProperty("confidenceScore")] + public double ConfidenceScore { get; set; } + + /// + /// Gets or sets the source from which the QnA was extracted. + /// + /// + /// The source from which the QnA was extracted. + /// + [JsonProperty(PropertyName = "source")] + public string? Source { get; set; } + + /// + /// Gets or sets the index of the answer in the knowledge base. V3 uses + /// 'qnaId', V4 uses 'id'. + /// + /// + /// The index of the answer in the knowledge base. V3 uses + /// 'qnaId', V4 uses 'id'. + /// + [JsonProperty(PropertyName = "id")] + public int Id { get; set; } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/Models/KnowledgeBaseAnswers.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/Models/KnowledgeBaseAnswers.cs new file mode 100644 index 000000000..77fb27104 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/Models/KnowledgeBaseAnswers.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Recognizers.Models +{ + /// + /// Contains answers for a user query. + /// + internal class KnowledgeBaseAnswers + { + /// + /// Gets the answers for a user query, + /// sorted in decreasing order of ranking score. + /// + /// + /// The answers for a user query, + /// sorted in decreasing order of ranking score. + /// + [JsonProperty("answers")] + public List Answers { get; } = new List(); + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/QuestionAnsweringConstants.cs b/packages/Recognizers/OrchestrationWorkflow/dotnet/QuestionAnsweringConstants.cs new file mode 100644 index 000000000..ff3822755 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/QuestionAnsweringConstants.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Bot.Components.Recognizers.CLURecognizer +{ + /// + /// The CLU Constants. + /// + [SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Constants.")] + public static class QuestionAnsweringConstants + { + public const string QnAMatchIntent = "QnAMatch"; + + public static class ResponseOptions + { + /// + /// The Question Answering response result key. + /// + public const string ResultKey = "result"; + } + } +} diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/README.md b/packages/Recognizers/OrchestrationWorkflow/dotnet/README.md new file mode 100644 index 000000000..a3b7374da --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/README.md @@ -0,0 +1,47 @@ +# Conversation Language Understanding (CLU) Recognizer + +## Summary +This recognizer helps you add a custom recognizer to an empty bot built with Bot Framework Composer in order to use the [Conversation Language Understanding Service](https://learn.microsoft.com/en-us/azure/cognitive-services/language-service/conversational-language-understanding/overview) in place of the now deprecated LUIS. + +## Installation +In order to enable the CLU recognizer, you must first install the [CLU recognizer package](https://www.nuget.org/packages/Microsoft.Bot.Components.Recognizers.CLURecognizer) from NuGet in your Composer project. + +1. Create a new Composer bot using the `Empty Bot` template. + +2. Open the Package Manager in Composer. + +3. Search for `ConversationLanguageUnderstandingRecognizer` and install the package. + +## Configuration +To enable the Conversation Language Understanding recognizer, complete the following steps: + +1. Select `Custom` as your root dialog's recognizer type. + +2. Paste the following JSON into the custom recognizer configuration window: + +```json +{ + "$kind": "Microsoft.CluRecognizer", + "projectName": "", + "endpoint": "", + "endpointKey": "", + "deploymentName": "" +} +``` +3. Update the `projectName`, `endpoint`, `endpointKey`, and `deploymentName` fields with the values from your Conversation Language Understanding service. + + - The `deploymentName` value can be found in the `Deploying a model` blade under `Deployments` inside `Language Studio`. + + - The `endpoint` value can be found in the `Deploying a model` blade under `Deployments` inside `Language Studio` by clicking on the `Get prediction URL` option. It can also be found in the `Keys & Endpoint` blade of your Language resource in the Azure Portal. The endpoint should take the format `https://.cognitiveservices.azure.com`. Ensure that the trailing slash is *omitted*. + + - The `projectName` and `endpointKey` values can be found in your `Project Settings` blade under `Azure Language Resource` inside `Language Studio`. + +Ensure that you have selected the correct values for each field. Using the wrong values can lead to errors when running the bot. + +## Usage +Once you have configured intents and entities in your Conversation Language Understanding project, custom intent triggers and the CLU intent triggers should function as normal. When creating a new intent trigger in a Composer bot, make sure that the `intent` value of the trigger matches the intent in the deployed Language resource (case-sensitive). + +Since the Conversation Language Understanding recognizer is a modified version of the existing LUIS recognizer, the workflow elements work the same. In addition the respective LUIS events and telemetry are written out to the logs. + +## Limitations +Please remember that Composer does not integrate natively with Conversation Language Understanding, so managing intents and entities **must** be done in the Language Studio portal instead of Composer. \ No newline at end of file diff --git a/packages/Recognizers/OrchestrationWorkflow/dotnet/Schemas/Microsoft.CluRecognizer.schema b/packages/Recognizers/OrchestrationWorkflow/dotnet/Schemas/Microsoft.CluRecognizer.schema new file mode 100644 index 000000000..b98259fb2 --- /dev/null +++ b/packages/Recognizers/OrchestrationWorkflow/dotnet/Schemas/Microsoft.CluRecognizer.schema @@ -0,0 +1,56 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IRecognizer)", + "title": "CLU Recognizer", + "description": "CLU recognizer.", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Optional unique id using with RecognizerSet. Other recognizers should return 'DeferToRecognizer_{Id}' intent when cross training data for this recognizer." + }, + "projectName": { + "$ref": "schema:#/definitions/stringExpression", + "title": "CLU project name", + "description": "The project name of your CLU service." + }, + "deploymentName": { + "$ref": "schema:#/definitions/stringExpression", + "title": "CLU deployment name", + "description": "The deployment name for your CLU service." + }, + "endpoint": { + "$ref": "schema:#/definitions/stringExpression", + "title": "CLU endpoint", + "description": "Endpoint to use for CLU service like https://{your-clu-service-name}.cognitiveservices.azure.com." + }, + "endpointKey": { + "$ref": "schema:#/definitions/stringExpression", + "title": "CLU endpoint key", + "description": "The endpoint key for your CLU service." + }, + "includeAPIResults": { + "$ref": "schema:#/definitions/booleanExpression", + "title": "Include CLU API results", + "description": "Optional gets or sets a value indicating whether CLU API results should be included in the RecognizerResult returned. If null, then defaults to false." + }, + "cluRequestBodyStringIndexType": { + "$ref": "schema:#/definitions/stringExpression", + "title": "The string index type to include in the CLU request body", + "description": "Optional value indicating the string index type to include in the the CLU request body. If null, then the TextElement_V8 string value is used." + }, + "cluApiVersion": { + "$ref": "schema:#/definitions/stringExpression", + "title": "CLU API version", + "description": "Optional CLU version to target. If null, then the 2022-05-01 string value is used." + } + }, + "required": [ + "projectName", + "endpoint", + "endpointKey", + "deploymentName" + ] +}