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"
+ ]
+}