• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 380327

08 Dec 2023 07:19PM UTC coverage: 78.458% (+0.006%) from 78.452%
380327

Pull #6711

CI-PR build

web-flow
Merge a07471e77 into 979413cd7
Pull Request #6711: Add obsolete warning to Orchestrator classes

26135 of 33311 relevant lines covered (78.46%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.43
/libraries/Microsoft.Bot.Builder.AI.Orchestrator/OrchestratorRecognizer.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Concurrent;
6
using System.Collections.Generic;
7
using System.Globalization;
8
using System.IO;
9
using System.Linq;
10
using System.Runtime.CompilerServices;
11
using System.Text;
12
using System.Threading;
13
using System.Threading.Tasks;
14
using AdaptiveExpressions.Properties;
15
using Microsoft.Bot.Builder.Dialogs;
16
using Microsoft.Bot.Builder.Dialogs.Adaptive.Recognizers;
17
using Microsoft.Bot.Builder.TraceExtensions;
18
using Microsoft.BotFramework.Orchestrator;
19
using Newtonsoft.Json;
20
using Newtonsoft.Json.Linq;
21

22
namespace Microsoft.Bot.Builder.AI.Orchestrator
23
{
24
    /// <summary>
25
    /// Class that represents an adaptive Orchestrator recognizer.
26
    /// </summary>
27
    [Obsolete("The Bot Framework Orchestrator will be deprecated in the next version of the Bot Framework SDK.")]
28
    public class OrchestratorRecognizer : AdaptiveRecognizer
29
    {
30
        /// <summary>
31
        /// The Kind name for this recognizer.
32
        /// </summary>
33
        [JsonProperty("$kind")]
34
        public const string Kind = "Microsoft.OrchestratorRecognizer";
35

36
        /// <summary>
37
        /// Property key in RecognizerResult that holds the full recognition result from Orchestrator core.
38
        /// </summary>
39
        public const string ResultProperty = "result";
40

41
        /// <summary>
42
        /// Property key used when storing extracted entities in a custom event within telemetry.
43
        /// </summary>
44
        public const string EntitiesProperty = "entityResult";
45

46
        private const float UnknownIntentFilterScore = 0.4F;
47
        private static ConcurrentDictionary<string, OrchestratorDictionaryEntry> orchestratorMap = new ConcurrentDictionary<string, OrchestratorDictionaryEntry>();
1✔
48
        private OrchestratorDictionaryEntry _orchestrator = null;
49
        private ILabelResolver _resolver = null;
50
        private bool _isResolverMockup = false;
51
        private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { MaxDepth = null };
1✔
52

53
        /// <summary>
54
        /// Initializes a new instance of the <see cref="OrchestratorRecognizer"/> class.
55
        /// </summary>
56
        /// <param name="callerLine">Caller line.</param>
57
        /// <param name="callerPath">Caller path.</param>
58
        [JsonConstructor]
59
        public OrchestratorRecognizer([CallerFilePath] string callerPath = "", [CallerLineNumber] int callerLine = 0)
60
            : base(callerPath, callerLine)
1✔
61
        {
62
        }
1✔
63

64
        /// <summary>
65
        /// Initializes a new instance of the <see cref="OrchestratorRecognizer"/> class.
66
        /// </summary>
67
        /// <param name="modelFolder">Specifies the base model folder.</param>
68
        /// <param name="snapshotFile">Specifies full path to the snapshot file.</param>
69
        /// <param name="resolverExternal">External label resolver object.</param>
70
        public OrchestratorRecognizer(string modelFolder, string snapshotFile, ILabelResolver resolverExternal = null)
1✔
71
        {
72
            InitializeModel(modelFolder, snapshotFile, resolverExternal);
1✔
73
        }
1✔
74

75
        /// <summary>
76
        /// Gets or sets the folder path to Orchestrator base model to use.
77
        /// </summary>
78
        /// <value>
79
        /// Model path.
80
        /// </value>
81
        [JsonProperty("modelFolder")]
82
        public StringExpression ModelFolder { get; set; } = "=settings.orchestrator.modelFolder";
1✔
83

84
        /// <summary>
85
        /// Gets or sets the full path to Orchestrator snapshot file to use.
86
        /// </summary>
87
        /// <value>
88
        /// Snapshot path.
89
        /// </value>
90
        [JsonProperty("snapshotFile")]
91
        public StringExpression SnapshotFile { get; set; } = "=settings.orchestrator.snapshotFile";
1✔
92

93
        /// <summary>
94
        /// Gets or sets an external entity recognizer.
95
        /// </summary>
96
        /// <remarks>This recognizer is run before calling Orchestrator and the entities are merged with Orchestrator results.</remarks>
97
        /// <value>Recognizer.</value>
98
        [JsonProperty("externalEntityRecognizer")]
99
        public Recognizer ExternalEntityRecognizer { get; set; }
1✔
100

101
        /// <summary>
102
        /// Gets or sets the disambiguation score threshold.
103
        /// </summary>
104
        /// <value>
105
        /// Recognizer returns ChooseIntent (disambiguation) if other intents are classified within this threshold of the top scoring intent.
106
        /// </value>
107
        [JsonProperty("disambiguationScoreThreshold")]
108
        public NumberExpression DisambiguationScoreThreshold { get; set; } = 0.05F;
1✔
109

110
        /// <summary>
111
        /// Gets or sets detect ambiguous intents.
112
        /// </summary>
113
        /// <value>
114
        /// When true, recognizer will look for ambiguous intents - those within specified threshold to top scoring intent.
115
        /// </value>
116
        [JsonProperty("detectAmbiguousIntents")]
117
        public BoolExpression DetectAmbiguousIntents { get; set; } = false;
1✔
118

119
        /// <summary>
120
        /// Gets or sets a value indicating whether to enable or disable entity-extraction logic.
121
        /// NOTE: SHOULD consider removing this flag in the next major SDK release (V5).
122
        /// </summary>
123
        /// <value>
124
        /// The flag for enabling or disabling entity-extraction function.
125
        /// </value>
126
        public bool ScoreEntities { get; set; } = true;
×
127

128
        /// <summary>
129
        /// Return recognition results.
130
        /// </summary>
131
        /// <param name="dc">Context object containing information for a single turn of conversation with a user.</param>
132
        /// <param name="activity">The incoming activity received from the user. The Text property value is used as the query text for QnA Maker.</param>
133
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
134
        /// <param name="telemetryProperties">Additional properties to be logged to telemetry with the LuisResult event.</param>
135
        /// <param name="telemetryMetrics">Additional metrics to be logged to telemetry with the LuisResult event.</param>
136
        /// <returns>A <see cref="RecognizerResult"/> containing the QnA Maker result.</returns>
137
        public override async Task<RecognizerResult> RecognizeAsync(DialogContext dc, Schema.Activity activity, CancellationToken cancellationToken, Dictionary<string, string> telemetryProperties = null, Dictionary<string, double> telemetryMetrics = null)
138
        {
139
            if (_resolver == null)
1✔
140
            {
141
                string modelFolder = ModelFolder.GetValue(dc.State);
×
142
                string snapshotFile = SnapshotFile.GetValue(dc.State);
×
143
                InitializeModel(modelFolder, snapshotFile, null);
×
144
            }
145

146
            var text = activity.Text ?? string.Empty;
1✔
147
            var detectAmbiguity = DetectAmbiguousIntents.GetValue(dc.State);
1✔
148

149
            var recognizerResult = new RecognizerResult()
1✔
150
            {
1✔
151
                Text = text,
1✔
152
                Intents = new Dictionary<string, IntentScore>(),
1✔
153
            };
1✔
154

155
            if (string.IsNullOrWhiteSpace(text))
1✔
156
            {
157
                // nothing to recognize, return empty recognizerResult
158
                return recognizerResult;
1✔
159
            }
160

161
            // Score with orchestrator
162
            var results = _resolver.Score(text)?.ToList();
×
163

164
            if ((results != null) && results.Any())
1✔
165
            {
166
                // Add full recognition result as a 'result' property
167
                recognizerResult.Properties.Add(ResultProperty, results);
1✔
168

169
                var topScore = results[0].Score;
1✔
170

171
                // if top scoring intent is less than threshold, return None
172
                if (topScore < UnknownIntentFilterScore)
1✔
173
                {
174
                    // remove existing None intents
175
                    for (int i = 0; i < results.Count; ++i)
1✔
176
                    {
177
                        if (results[i].Label.Name == NoneIntent)
1✔
178
                        {
179
                            results.RemoveAt(i--);
1✔
180
                        }
181
                    }
182

183
                    results.Insert(0, new Result() { Score = 1.0, Label = new Label() { Name = NoneIntent, Type = LabelType.Intent } });
1✔
184
                    foreach (var result in results)
1✔
185
                    {
186
                        recognizerResult.Intents.Add(result.Label.Name, new IntentScore()
1✔
187
                        {
1✔
188
                            Score = result.Score
1✔
189
                        });
1✔
190
                    }
191
                }
192
                else
193
                {
194
                    // add top score
195
                    foreach (var result in results)
1✔
196
                    {
197
                        recognizerResult.Intents.Add(result.Label.Name, new IntentScore()
1✔
198
                        {
1✔
199
                            Score = result.Score
1✔
200
                        });
1✔
201
                    }
202

203
                    // Disambiguate if configured
204
                    if (detectAmbiguity)
1✔
205
                    {
206
                        var thresholdScore = DisambiguationScoreThreshold.GetValue(dc.State);
1✔
207
                        var classifyingScore = Math.Round(topScore, 2) - Math.Round(thresholdScore, 2);
1✔
208
                        var ambiguousResults = results.Where(item => item.Score >= classifyingScore).ToList();
1✔
209

210
                        if (ambiguousResults.Count > 1)
1✔
211
                        {
212
                            // create a RecognizerResult for each ambiguous result.
213
                            var recognizerResults = ambiguousResults.Select(result => new RecognizerResult()
1✔
214
                            {
1✔
215
                                Text = text,
1✔
216
                                AlteredText = result.ClosestText,
1✔
217
                                Entities = recognizerResult.Entities,
1✔
218
                                Properties = recognizerResult.Properties,
1✔
219
                                Intents = new Dictionary<string, IntentScore>()
1✔
220
                                {
1✔
221
                                    { result.Label.Name, new IntentScore() { Score = result.Score } }
1✔
222
                                },
1✔
223
                            });
1✔
224

225
                            // replace RecognizerResult with ChooseIntent => Ambiguous recognizerResults as candidates.
226
                            recognizerResult = CreateChooseIntentResult(recognizerResults.ToDictionary(result => Guid.NewGuid().ToString(), result => result));
1✔
227
                        }
228
                    }
229
                }
230
            }
231
            else
232
            {
233
                // Return 'None' if no intent matched.
234
                recognizerResult.Intents.Add(NoneIntent, new IntentScore() { Score = 1.0 });
×
235
            }
236

237
            if (ExternalEntityRecognizer != null)
1✔
238
            {
239
                // Run external recognition
240
                var externalResults = await ExternalEntityRecognizer.RecognizeAsync(dc, activity, cancellationToken, telemetryProperties, telemetryMetrics).ConfigureAwait(false);
1✔
241
                recognizerResult.Entities = externalResults.Entities;
1✔
242
            }
243

244
            TryScoreEntities(text, recognizerResult);
1✔
245

246
            // Add full recognition result as a 'result' property
247
            await dc.Context.TraceActivityAsync($"{nameof(OrchestratorRecognizer)}Result", JObject.FromObject(recognizerResult), nameof(OrchestratorRecognizer), "Orchestrator Recognition", cancellationToken).ConfigureAwait(false);
1✔
248
            TrackRecognizerResult(dc, $"{nameof(OrchestratorRecognizer)}Result", FillRecognizerResultTelemetryProperties(recognizerResult, telemetryProperties, dc), telemetryMetrics);
1✔
249

250
            return recognizerResult;
1✔
251
        }
1✔
252

253
        /// <summary>
254
        /// Uses the RecognizerResult to create a list of properties to be included when tracking the result in telemetry.
255
        /// </summary>
256
        /// <param name="recognizerResult">Recognizer Result.</param>
257
        /// <param name="telemetryProperties">A list of properties to append or override the properties created using the RecognizerResult.</param>
258
        /// <param name="dialogContext">Dialog Context.</param>
259
        /// <returns>A dictionary that can be included when calling the TrackEvent method on the TelemetryClient.</returns>
260
        protected override Dictionary<string, string> FillRecognizerResultTelemetryProperties(RecognizerResult recognizerResult, Dictionary<string, string> telemetryProperties, DialogContext dialogContext = null)
261
        {
262
            if (dialogContext == null)
1✔
263
            {
264
                throw new ArgumentNullException(nameof(dialogContext), "DialogContext needed for state in AdaptiveRecognizer.FillRecognizerResultTelemetryProperties method.");
1✔
265
            }
266

267
            var orderedIntents = recognizerResult.Intents.Any() ? recognizerResult.Intents.OrderByDescending(key => key.Value.Score) : null;
1✔
268
            var properties = new Dictionary<string, string>
×
269
            {
×
270
                { "TopIntent", recognizerResult.Intents.Any() ? orderedIntents.First().Key : null },
×
271
                { "TopIntentScore", recognizerResult.Intents.Any() ? orderedIntents.First().Value?.Score?.ToString("N1", CultureInfo.InvariantCulture) : null },
×
272
                { "NextIntent", recognizerResult.Intents.Count > 1 ? orderedIntents.ElementAtOrDefault(1).Key : null },
×
273
                { "NextIntentScore", recognizerResult.Intents.Count > 1 ? orderedIntents.ElementAtOrDefault(1).Value?.Score?.ToString("N1", CultureInfo.InvariantCulture) : null },
×
274
                { "Intents", recognizerResult.Intents.Any() ? JsonConvert.SerializeObject(recognizerResult.Intents, _settings) : null },
×
275
                { "Entities", recognizerResult.Entities?.ToString() },
×
276
                { "AdditionalProperties", recognizerResult.Properties.Any() ? JsonConvert.SerializeObject(recognizerResult.Properties, _settings) : null },
×
277
            };
×
278

279
            var (logPersonalInfo, error) = LogPersonalInformation.TryGetValue(dialogContext.State);
1✔
280

281
            if (logPersonalInfo && !string.IsNullOrEmpty(recognizerResult.Text))
1✔
282
            {
283
                properties.Add("Text", recognizerResult.Text);
1✔
284
                properties.Add("AlteredText", recognizerResult.AlteredText);
1✔
285
            }
286

287
            // Additional Properties can override "stock" properties.
288
            if (telemetryProperties != null)
1✔
289
            {
290
                return telemetryProperties.Concat(properties)
×
291
                    .GroupBy(kv => kv.Key)
×
292
                    .ToDictionary(g => g.Key, g => g.First().Value);
×
293
            }
294

295
            return properties;
1✔
296
        }
297

298
        private static JToken EntityResultToJObject(string text, Result result)
299
        {
300
            Span span = result.Label.Span;
1✔
301
            return new JObject(
1✔
302
                new JProperty("type", result.Label.Name),
1✔
303
                new JProperty("score", result.Score),
1✔
304
                new JProperty("text", text.Substring((int)span.Offset, (int)span.Length)),
1✔
305
                new JProperty("start", (int)span.Offset),
1✔
306
                new JProperty("end", (int)(span.Offset + span.Length)));
1✔
307
        }
308

309
        private static JToken EntityResultToInstanceJObject(string text, Result result)
310
        {
311
            Span span = result.Label.Span;
1✔
312
            dynamic instance = new JObject();
1✔
313
            instance.startIndex = (int)span.Offset;
1✔
314
            instance.endIndex = (int)(span.Offset + span.Length);
1✔
315
            instance.score = result.Score;
1✔
316
            instance.text = text.Substring((int)span.Offset, (int)span.Length);
1✔
317
            instance.type = result.Label.Name;
1✔
318
            return instance;
1✔
319
        }
320

321
        private void TryScoreEntities(string text, RecognizerResult recognizerResult)
322
        {
323
            // It's impossible to extract entities without a _resolver object.
324
            if (_resolver == null)
1✔
325
            {
326
                return;
×
327
            }
328

329
            // Entity extraction can be controlled by the ScoreEntities flag.
330
            // NOTE: SHOULD consider removing this flag in the next major SDK release (V5).
331
            if (!this.ScoreEntities)
1✔
332
            {
333
                return;
×
334
            }
335

336
            // The following check is necessary to ensure that the _resolver object
337
            // is capable of entity exttraction. However, this check can also block
338
            // a mock-up _resolver.
339
            if (!_isResolverMockup)
1✔
340
            {
341
                if ((_orchestrator == null) || (!_orchestrator.IsEntityExtractionCapable))
×
342
                {
343
                    return;
×
344
                }
345
            }
346

347
            // As this method is TryScoreEntities, so it's best effort only, there should
348
            // not be any exception thrown out of this method.
349
            try
350
            {
351
                var results = _resolver.Score(text, LabelType.Entity);
1✔
352

353
                if ((results != null) && results.Any())
1✔
354
                {
355
                    recognizerResult.Properties.Add(EntitiesProperty, results);
1✔
356

357
                    if (recognizerResult.Entities == null)
1✔
358
                    {
359
                        recognizerResult.Entities = new JObject();
×
360
                    }
361

362
                    var entitiesResult = recognizerResult.Entities;
1✔
363
                    foreach (var result in results)
1✔
364
                    {
365
                        // add value
366
                        JToken values;
367
                        if (!entitiesResult.TryGetValue(result.Label.Name, StringComparison.OrdinalIgnoreCase, out values))
1✔
368
                        {
369
                            values = new JArray();
1✔
370
                            entitiesResult[result.Label.Name] = values;
1✔
371
                        }
372

373
                        // values came from an external entity recognizer, which may not make it a JArray.
374
                        if (values.Type != JTokenType.Array)
1✔
375
                        {
376
                            values = new JArray();
×
377
                        }
378

379
                        ((JArray)values).Add(EntityResultToJObject(text, result));
1✔
380

381
                        // get/create $instance
382
                        JToken instanceRoot;
383
                        if (!recognizerResult.Entities.TryGetValue("$instance", StringComparison.OrdinalIgnoreCase, out instanceRoot))
1✔
384
                        {
385
                            instanceRoot = new JObject();
×
386
                            recognizerResult.Entities["$instance"] = instanceRoot;
×
387
                        }
388

389
                        // instanceRoot came from an external entity recognizer, which may not make it a JObject.
390
                        if (instanceRoot.Type != JTokenType.Object)
1✔
391
                        {
392
                            instanceRoot = new JObject();
×
393
                        }
394

395
                        // add instanceData
396
                        JToken instanceData;
397
                        if (!((JObject)instanceRoot).TryGetValue(result.Label.Name, StringComparison.OrdinalIgnoreCase, out instanceData))
1✔
398
                        {
399
                            instanceData = new JArray();
1✔
400
                            instanceRoot[result.Label.Name] = instanceData;
1✔
401
                        }
402

403
                        // instanceData came from an external entity recognizer, which may not make it a JArray.
404
                        if (instanceData.Type != JTokenType.Array)
1✔
405
                        {
406
                            instanceData = new JArray();
×
407
                        }
408

409
                        ((JArray)instanceData).Add(EntityResultToInstanceJObject(text, result));
1✔
410
                    }
411
                }
412
            }
1✔
413
            catch (ApplicationException)
×
414
            {
415
                return; // ---- This is a "Try" function, i.e., best effort only, no exception.
×
416
            }
417
        }
1✔
418

419
        [MethodImpl(MethodImplOptions.Synchronized)]
420
        private void InitializeModel(string modelFolder, string snapshotFile, ILabelResolver resolverExternal = null)
421
        {
422
            if (resolverExternal != null)
1✔
423
            {
424
                _resolver = resolverExternal;
1✔
425
                _isResolverMockup = true;
1✔
426
                return;
1✔
427
            }
428

429
            {
430
                if (string.IsNullOrWhiteSpace(modelFolder))
1✔
431
                {
432
                    throw new ArgumentNullException(nameof(modelFolder));
1✔
433
                }
434

435
                if (string.IsNullOrWhiteSpace(snapshotFile))
1✔
436
                {
437
                    throw new ArgumentNullException(nameof(snapshotFile));
×
438
                }
439
            }
440

441
            var fullModelFolder = Path.GetFullPath(PathUtils.NormalizePath(modelFolder));
1✔
442

443
            _orchestrator = orchestratorMap.GetOrAdd(fullModelFolder, path =>
1✔
444
            {
1✔
445
                // Create Orchestrator
1✔
446
                string entityModelFolder = null;
1✔
447
                bool isEntityExtractionCapable = false;
1✔
448
                try
1✔
449
                {
1✔
450
                    entityModelFolder = Path.Combine(path, "entity");
1✔
451
                    isEntityExtractionCapable = Directory.Exists(entityModelFolder);
1✔
452

1✔
453
                    return new OrchestratorDictionaryEntry()
×
454
                    {
×
455
                        Orchestrator = isEntityExtractionCapable ?
×
456
                            new BotFramework.Orchestrator.Orchestrator(path, entityModelFolder) :
×
457
                            new BotFramework.Orchestrator.Orchestrator(path),
×
458
                        IsEntityExtractionCapable = isEntityExtractionCapable
×
459
                    };
×
460
                }
1✔
461
                catch (Exception ex)
1✔
462
                {
1✔
463
                    throw new InvalidOperationException(
×
464
                        isEntityExtractionCapable ? $"Failed to find or load Model with path {path}, entity model path {entityModelFolder}" : $"Failed to find or load Model with path {path}",
×
465
                        ex);
×
466
                }
1✔
467
            });
×
468

469
            var fullSnapShotFile = Path.GetFullPath(PathUtils.NormalizePath(snapshotFile));
×
470

471
            // Load the snapshot
472
            byte[] snapShotByteArray = File.ReadAllBytes(fullSnapShotFile);
×
473

474
            // Create label resolver
475
            _resolver = this._orchestrator.Orchestrator.CreateLabelResolver(snapShotByteArray);
×
476
        }
×
477

478
        /// <summary>
479
        /// OrchestratorDictionaryEntry is used for the static orchestratorMap object.
480
        /// </summary>
481
        private class OrchestratorDictionaryEntry
482
        {
483
            /// <summary>
484
            /// Gets or sets the Orchestrator object.
485
            /// </summary>
486
            /// <value>
487
            /// The Orchestrator object.
488
            /// </value>
489
            public BotFramework.Orchestrator.Orchestrator Orchestrator
490
            {
491
                get;
×
492
                set;
×
493
            }
494

495
            /// <summary>
496
            /// Gets or sets a value indicating whether the Orchestrator object is capable of entity extraction.
497
            /// </summary>
498
            /// <value>
499
            /// The IsEntityExtractionCapable flag.
500
            /// </value>
501
            public bool IsEntityExtractionCapable
502
            {
503
                get;
×
504
                set;
×
505
            }
506
        }
507
    }
508
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc