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

microsoft / botbuilder-dotnet / 388636

03 May 2024 02:02PM UTC coverage: 78.171% (+0.02%) from 78.153%
388636

push

CI-PR build

web-flow
Microsoft.Identity.Client bump (#6779)

* Microsoft.Identity.Client bump

* Compensated for new ManagedIdentityClient auto-retrying requests

---------

Co-authored-by: Tracy Boehrer <trboehre@microsoft.com>

26189 of 33502 relevant lines covered (78.17%)

0.78 hits per line

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

88.79
/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/AdaptiveDialog.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.ComponentModel;
7
using System.Globalization;
8
using System.Linq;
9
using System.Runtime.CompilerServices;
10
using System.Text;
11
using System.Threading;
12
using System.Threading.Tasks;
13
using AdaptiveExpressions.Properties;
14
using Microsoft.Bot.Builder.Dialogs.Adaptive.Conditions;
15
using Microsoft.Bot.Builder.Dialogs.Adaptive.Recognizers;
16
using Microsoft.Bot.Builder.Dialogs.Adaptive.Selectors;
17
using Microsoft.Bot.Builder.Dialogs.Debugging;
18
using Microsoft.Bot.Builder.Dialogs.Declarative.Resources;
19
using Microsoft.Bot.Schema;
20
using Newtonsoft.Json;
21
using Newtonsoft.Json.Linq;
22

23
namespace Microsoft.Bot.Builder.Dialogs.Adaptive
24
{
25
    /// <summary>
26
    /// The Adaptive Dialog models conversation using events and events to adapt dynamicaly to changing conversation flow.
27
    /// </summary>
28
    public class AdaptiveDialog : DialogContainer, IDialogDependencies
29
    {
30
        /// <summary>
31
        /// Class identifier.
32
        /// </summary>
33
        [JsonProperty("$kind")]
34
        public const string Kind = "Microsoft.AdaptiveDialog";
35

36
        internal const string ConditionTracker = "dialog._tracker.conditions";
37

38
        private const string AdaptiveKey = "_adaptive";
39
        private const string DefaultOperationKey = "$defaultOperation";
40
        private const string ExpectedOnlyKey = "$expectedOnly";
41
        private const string InstanceKey = "$instance";
42
        private const string NoneIntentKey = "None";
43
        private const string OperationsKey = "$operations";
44
        private const string PropertyEnding = "Property";
45
        private const string RequiresValueKey = "$requiresValue";
46
        private const string UtteranceKey = "utterance";
47

48
        // unique key for change tracking of the turn state (TURN STATE ONLY)
49
        private readonly string changeTurnKey = Guid.NewGuid().ToString();
1✔
50

51
        private RecognizerSet recognizerSet = new RecognizerSet();
1✔
52

53
        private object syncLock = new object();
1✔
54
        private bool installedDependencies;
55

56
        private bool needsTracker = false;
57

58
        private SchemaHelper dialogSchema;
59

60
        private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { MaxDepth = null };
1✔
61

62
        /// <summary>
63
        /// Initializes a new instance of the <see cref="AdaptiveDialog"/> class.
64
        /// </summary>
65
        /// <param name="dialogId">Optional, dialog identifier.</param>
66
        /// <param name="callerPath">Optional, source file full path.</param>
67
        /// <param name="callerLine">Optional, line number in source file.</param>
68
        public AdaptiveDialog(string dialogId = null, [CallerFilePath] string callerPath = "", [CallerLineNumber] int callerLine = 0)
69
            : base(dialogId)
1✔
70
        {
71
            RegisterSourceLocation(callerPath, callerLine);
1✔
72
        }
1✔
73

74
        /// <summary>
75
        /// Gets or sets recognizer for processing incoming user input.
76
        /// </summary>
77
        /// <value>
78
        /// Recognizer for processing incoming user input.
79
        /// </value>
80
        [JsonProperty("recognizer")]
81
        public Recognizer Recognizer { get; set; }
1✔
82

83
        /// <summary>
84
        /// Gets or sets language Generator override.
85
        /// </summary>
86
        /// <value>
87
        /// Language Generator override.
88
        /// </value>
89
        [JsonProperty("generator")]
90
        public LanguageGenerator Generator { get; set; }
1✔
91

92
        /// <summary>
93
        /// Gets or sets trigger handlers to respond to conditions which modifying the executing plan. 
94
        /// </summary>
95
        /// <value>
96
        /// Trigger handlers to respond to conditions which modifying the executing plan. 
97
        /// </value>
98
        [JsonProperty("triggers")]
99
#pragma warning disable CA2227 // Collection properties should be read only (we can't change this without breaking binary compat)
100
        public virtual List<OnCondition> Triggers { get; set; } = new List<OnCondition>();
1✔
101
#pragma warning restore CA2227 // Collection properties should be read only
102

103
        /// <summary>
104
        /// Gets or sets an expression indicating whether to end the dialog when there are no actions to execute.
105
        /// </summary>
106
        /// <remarks>
107
        /// If true, when there are no actions to execute, the current dialog will end
108
        /// If false, when there are no actions to execute, the current dialog will simply end the turn and still be active.
109
        /// </remarks>
110
        /// <value>
111
        /// Whether to end the dialog when there are no actions to execute.
112
        /// </value>
113
        [DefaultValue(true)]
114
        [JsonProperty("autoEndDialog")]
115
        public BoolExpression AutoEndDialog { get; set; } = true;
1✔
116

117
        /// <summary>
118
        /// Gets or sets the selector for picking the possible events to execute.
119
        /// </summary>
120
        /// <value>
121
        /// The selector for picking the possible events to execute.
122
        /// </value>
123
        [JsonProperty("selector")]
124
        public TriggerSelector Selector { get; set; }
1✔
125

126
        /// <summary>
127
        /// Gets or sets the property to return as the result when the dialog ends when there are no more Actions and AutoEndDialog = true.
128
        /// </summary>
129
        /// <value>
130
        /// The property to return as the result when the dialog ends when there are no more Actions and AutoEndDialog = true.
131
        /// </value>
132
        [JsonProperty("defaultResultProperty")]
133
        public string DefaultResultProperty { get; set; } = "dialog.result";
1✔
134

135
        /// <summary>
136
        /// Gets or sets schema that describes what the dialog works over.
137
        /// </summary>
138
        /// <value>JSON Schema for the dialog.</value>
139
        [JsonProperty("schema")]
140
#pragma warning disable CA2227 // Collection properties should be read only
141
        public JObject Schema
142
#pragma warning restore CA2227 // Collection properties should be read only
143
        {
144
            get => dialogSchema?.Schema;
1✔
145
            set
146
            {
147
                dialogSchema = new SchemaHelper(value);
1✔
148
            }
1✔
149
        }
150

151
        /// <summary>
152
        /// Called when the dialog is started and pushed onto the dialog stack.
153
        /// </summary>
154
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
155
        /// <param name="options">Optional, initial information to pass to the dialog.</param>
156
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects
157
        /// or threads to receive notice of cancellation.</param>
158
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
159
        public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default)
160
        {
161
            if (options is CancellationToken)
1✔
162
            {
163
                throw new ArgumentException($"{nameof(options)} should not ever be a cancellation token");
×
164
            }
165

166
            EnsureDependenciesInstalled();
1✔
167

168
            await this.CheckForVersionChangeAsync(dc, cancellationToken).ConfigureAwait(false);
1✔
169

170
            // replace initial activeDialog.State with clone of options
171
            if (options != null)
1✔
172
            {
173
                dc.ActiveDialog.State = JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject(options, _settings), _settings);
1✔
174
            }
175

176
            if (!dc.State.ContainsKey(DialogPath.EventCounter))
1✔
177
            {
178
                dc.State.SetValue(DialogPath.EventCounter, 0u);
1✔
179
            }
180

181
            if (dialogSchema != null && !dc.State.ContainsKey(DialogPath.RequiredProperties))
1✔
182
            {
183
                // RequiredProperties control what properties must be filled in.
184
                // Initialize if not present from schema.
185
                dc.State.SetValue(DialogPath.RequiredProperties, dialogSchema.Required());
1✔
186
            }
187

188
            if (needsTracker)
1✔
189
            {
190
                if (!dc.State.ContainsKey(ConditionTracker))
1✔
191
                {
192
                    foreach (var trigger in Triggers)
1✔
193
                    {
194
                        if (trigger.RunOnce && trigger.Condition != null)
1✔
195
                        {
196
                            var paths = dc.State.TrackPaths(trigger.Condition.ToExpression().References());
1✔
197
                            var triggerPath = $"{ConditionTracker}.{trigger.Id}.";
1✔
198
                            dc.State.SetValue(triggerPath + "paths", paths);
1✔
199
                            dc.State.SetValue(triggerPath + "lastRun", 0u);
1✔
200
                        }
201
                    }
202
                }
203
            }
204

205
            var activeDialogState = dc.ActiveDialog.State as Dictionary<string, object>;
1✔
206
            activeDialogState[AdaptiveKey] = new AdaptiveDialogState();
1✔
207

208
            // Evaluate events and queue up step changes
209
            var dialogEvent = new DialogEvent
1✔
210
            {
1✔
211
                Name = AdaptiveEvents.BeginDialog,
1✔
212
                Value = options,
1✔
213
                Bubble = false
1✔
214
            };
1✔
215
            
216
            var properties = new Dictionary<string, string>()
1✔
217
                {
1✔
218
                    { "DialogId", Id },
1✔
219
                    { "Kind", Kind },
1✔
220
                    { "context", TelemetryLoggerConstants.DialogStartEvent }
1✔
221
                };
1✔
222
            TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
223
            TelemetryClient.TrackDialogView(Id);
1✔
224

225
            await OnDialogEventAsync(dc, dialogEvent, cancellationToken).ConfigureAwait(false);
1✔
226

227
            // Continue step execution
228
            return await ContinueActionsAsync(dc, options, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
229
        }
1✔
230

231
        /// <summary>
232
        /// Called when the dialog is _continued_, where it is the active dialog and the
233
        /// user replies with a new activity.
234
        /// </summary>
235
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
236
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects
237
        /// or threads to receive notice of cancellation.</param>
238
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
239
        public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
240
        {
241
            EnsureDependenciesInstalled();
1✔
242

243
            await this.CheckForVersionChangeAsync(dc, cancellationToken).ConfigureAwait(false);
1✔
244

245
            // Continue step execution
246
            return await ContinueActionsAsync(dc, options: null, cancellationToken).ConfigureAwait(false);
1✔
247
        }
1✔
248

249
        /// <summary>
250
        /// Called when a child dialog completed its turn, returning control to this dialog.
251
        /// </summary>
252
        /// <param name="dc">The dialog context for the current turn of the conversation.</param>
253
        /// <param name="reason">Reason why the dialog resumed.</param>
254
        /// <param name="result">Optional, value returned from the dialog that was called. The type
255
        /// of the value returned is dependent on the child dialog.</param>
256
        /// <param name="cancellationToken">Optional, A <see cref="CancellationToken"/> that can be used by other objects
257
        /// or threads to receive notice of cancellation.</param>
258
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
259
        public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
260
        {
261
            if (result is CancellationToken)
×
262
            {
263
                throw new ArgumentException($"{nameof(result)} cannot be a cancellation token");
×
264
            }
265

266
            await this.CheckForVersionChangeAsync(dc, cancellationToken).ConfigureAwait(false);
×
267

268
            // Containers are typically leaf nodes on the stack but the dev is free to push other dialogs
269
            // on top of the stack which will result in the container receiving an unexpected call to
270
            // resumeDialog() when the pushed on dialog ends.
271
            // To avoid the container prematurely ending we need to implement this method and simply
272
            // ask our inner dialog stack to re-prompt.
273
            await RepromptDialogAsync(dc.Context, dc.ActiveDialog, cancellationToken).ConfigureAwait(false);
×
274

275
            return EndOfTurn;
×
276
        }
×
277

278
        /// <summary>
279
        /// Called when the dialog is ending.
280
        /// </summary>
281
        /// <param name="turnContext">The context object for this turn.</param>
282
        /// <param name="instance">State information associated with the instance of this dialog on the dialog stack.</param>
283
        /// <param name="reason">Reason why the dialog ended.</param>
284
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects
285
        /// or threads to receive notice of cancellation.</param>
286
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
287
        public override Task EndDialogAsync(ITurnContext turnContext, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken = default)
288
        {
289
            var properties = new Dictionary<string, string>()
1✔
290
                {
1✔
291
                    { "DialogId", Id },
1✔
292
                    { "Kind", Kind }
1✔
293
                };
1✔
294

295
            if (reason == DialogReason.CancelCalled)
1✔
296
            {
297
                properties.Add("context", TelemetryLoggerConstants.DialogCancelEvent);
1✔
298
                TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
299
            }
300
            else if (reason == DialogReason.EndCalled)
1✔
301
            {
302
                properties.Add("context", TelemetryLoggerConstants.CompleteEvent);
1✔
303
                TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
304
            }
305

306
            return base.EndDialogAsync(turnContext, instance, reason, cancellationToken);
1✔
307
        }
308

309
        /// <summary>
310
        /// RepromptDialog with dialogContext.
311
        /// </summary>
312
        /// <remarks>AdaptiveDialogs use the DC, which is available because AdaptiveDialogs handle the new AdaptiveEvents.RepromptDialog.</remarks>
313
        /// <param name="dc">dc.</param>
314
        /// <param name="instance">instance.</param>
315
        /// <param name="cancellationToken">ct.</param>
316
        /// <returns>task.</returns>
317
        public virtual async Task RepromptDialogAsync(DialogContext dc, DialogInstance instance, CancellationToken cancellationToken = default)
318
        {
319
            // Forward to current sequence step
320
            var state = (instance.State as Dictionary<string, object>)[AdaptiveKey] as AdaptiveDialogState;
1✔
321

322
            if (state.Actions.Any())
1✔
323
            {
324
                // We need to mockup a DialogContext so that we can call RepromptDialog
325
                // for the active step
326
                var childContext = CreateChildContext(dc);
1✔
327
                await childContext.RepromptDialogAsync(cancellationToken).ConfigureAwait(false);
1✔
328
            }
329
        }
1✔
330

331
        /// <summary>
332
        /// Creates a child <see cref="DialogContext"/> for the given context.
333
        /// </summary>
334
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
335
        /// <returns>The child <see cref="DialogContext"/> or null if no <see cref="AdaptiveDialogState.Actions"/> are found for the given context.</returns>
336
        public override DialogContext CreateChildContext(DialogContext dc)
337
        {
338
            var activeDialogState = dc.ActiveDialog.State as Dictionary<string, object>;
1✔
339
            AdaptiveDialogState state = null;
1✔
340

341
            if (activeDialogState.TryGetValue(AdaptiveKey, out var currentState))
1✔
342
            {
343
                state = currentState as AdaptiveDialogState;
1✔
344
            }
345

346
            if (state == null)
1✔
347
            {
348
                state = new AdaptiveDialogState();
×
349
                activeDialogState[AdaptiveKey] = state;
×
350
            }
351

352
            if (state.Actions != null && state.Actions.Any())
1✔
353
            {
354
                var childContext = new DialogContext(this.Dialogs, dc, state.Actions.First());
1✔
355
                OnSetScopedServices(childContext);
1✔
356
                return childContext;
1✔
357
            }
358

359
            return null;
1✔
360
        }
361

362
        /// <summary>
363
        /// Gets <see cref="Dialog"/> enumerated dependencies.
364
        /// </summary>
365
        /// <returns><see cref="Dialog"/> enumerated dependencies.</returns>
366
        public IEnumerable<Dialog> GetDependencies()
367
        {
368
            EnsureDependenciesInstalled();
1✔
369

370
            // Expose required nested dependencies for parent dialog
371
            foreach (var dlg in Dialogs.GetDialogs())
1✔
372
            {
373
                if (dlg is IAdaptiveDialogDependencies dependencies)
1✔
374
                {
375
                    foreach (var item in dependencies.GetExternalDependencies())
1✔
376
                    {
377
                        yield return item;
1✔
378
                    }
379
                }
380
            }
381

382
            yield break;
1✔
383
        }
384

385
        /// <summary>
386
        /// Finds a child dialog that was previously added to the container.
387
        /// Uses DialogContext as fallback to gather the dialog from the <see cref="ResourceExplorer"/>.
388
        /// </summary>
389
        /// <param name="dialogId">The ID of the dialog to lookup.</param>
390
        /// <param name="dc">The dialog context where to find the dialog.</param>
391
        /// <returns>The Dialog if found; otherwise null.</returns>
392
        /// <remarks>
393
        /// When the Dialog is gathered from the <see cref="ResourceExplorer"/>,
394
        /// automatically will be loaded into the <see cref="DialogContext.Dialogs"/> stack.
395
        /// </remarks>
396
        public override Dialog FindDialog(string dialogId, DialogContext dc = null)
397
        {
398
            var dialog = FindDialog(dialogId);
1✔
399
            if (dialog != null)
1✔
400
            {
401
                return dialog;
×
402
            }
403

404
            if (dc == null)
1✔
405
            {
406
                throw new ArgumentNullException(nameof(dc));
×
407
            }
408

409
            var resourceExplorer = dc.Context.TurnState.Get<ResourceExplorer>();
1✔
410
            var resourceId = $"{dialogId}.dialog";
1✔
411
            var foundResource = resourceExplorer?.TryGetResource(resourceId, out _) ?? false;
×
412
            if (!foundResource)
1✔
413
            {
414
                return null;
1✔
415
            }
416
            
417
            dialog = resourceExplorer.LoadType<AdaptiveDialog>(resourceId);
1✔
418
            dialog.Id = dialogId;
1✔
419
            dc.Dialogs.Add(dialog);
1✔
420
            return dialog;
1✔
421
        }
422

423
        /// <summary>
424
        /// Gets the internal version string.
425
        /// </summary>
426
        /// <returns>Internal version string.</returns>
427
        protected override string GetInternalVersion()
428
        {
429
            StringBuilder sb = new StringBuilder();
1✔
430

431
            // change the container version if any dialogs are added or removed.
432
            sb.Append(this.Dialogs.GetVersion());
1✔
433

434
            // change version if the schema has changed.
435
            if (this.Schema != null)
1✔
436
            {
437
                sb.Append(JsonConvert.SerializeObject(this.Schema, _settings));
1✔
438
            }
439

440
            // change if triggers type/constraint change 
441
            foreach (var trigger in Triggers)
1✔
442
            {
443
                sb.Append(trigger.GetExpression().ToString());
1✔
444
            }
445

446
            return StringUtils.Hash(sb.ToString());
1✔
447
        }
448

449
        /// <summary>
450
        /// Called before an event is bubbled to its parent.
451
        /// </summary>
452
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
453
        /// <param name="dialogEvent">The <see cref="DialogEvent"/> being raised.</param>
454
        /// <param name="cancellationToken">Optional, the <see cref="CancellationToken"/> that can be used by other objects or threads to receive notice of cancellation.</param>
455
        /// <returns> Whether the event is handled by the current dialog and further processing should stop.</returns>
456
        protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, DialogEvent dialogEvent, CancellationToken cancellationToken = default)
457
        {
458
            var actionContext = ToActionContext(dc);
1✔
459

460
            // Process event and queue up any potential interruptions
461
            return await ProcessEventAsync(actionContext, dialogEvent, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
462
        }
1✔
463

464
        /// <summary>
465
        /// Called after an event was bubbled to all parents and wasn't handled.
466
        /// </summary>
467
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
468
        /// <param name="dialogEvent">The <see cref="DialogEvent"/> being raised.</param>
469
        /// <param name="cancellationToken">Optional, the <see cref="CancellationToken"/> that can be used by other objects or threads to receive notice of cancellation.</param>
470
        /// <returns> Whether the event is handled by the current dialog and further processing should stop.</returns>
471
        protected override async Task<bool> OnPostBubbleEventAsync(DialogContext dc, DialogEvent dialogEvent, CancellationToken cancellationToken = default)
472
        {
473
            var actionContext = ToActionContext(dc);
1✔
474

475
            // Process event and queue up any potential interruptions
476
            return await ProcessEventAsync(actionContext, dialogEvent, preBubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
477
        }
1✔
478

479
        /// <summary>
480
        /// Event processing implementation.
481
        /// </summary>
482
        /// <param name="actionContext">The <see cref="ActionContext"/> for the current turn of conversation.</param>
483
        /// <param name="dialogEvent">The <see cref="DialogEvent"/> being raised.</param>
484
        /// <param name="preBubble">A flag indicator for preBubble processing.</param>
485
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> used to signal this operation should be cancelled.</param>
486
        /// <returns>A <see cref="Task"/> representation of a boolean indicator or the result.</returns>
487
        protected virtual async Task<bool> ProcessEventAsync(ActionContext actionContext, DialogEvent dialogEvent, bool preBubble, CancellationToken cancellationToken = default(CancellationToken))
488
        {
489
            // Save into turn
490
            actionContext.State.SetValue(TurnPath.DialogEvent, dialogEvent);
1✔
491

492
            var activity = actionContext.State.GetValue<Activity>(TurnPath.Activity);
1✔
493

494
            // some dialogevents get promoted into turn state for general access outside of the dialogevent.
495
            // This allows events to be fired (in the case of ChooseIntent), or in interruption (Activity) 
496
            // Triggers all expressed against turn.recognized or turn.activity, and this mapping maintains that 
497
            // any event that is emitted updates those for the rest of rule evaluation.
498
            switch (dialogEvent.Name)
1✔
499
            {
500
                case AdaptiveEvents.RecognizedIntent:
501
                    {
502
                        // we have received a RecognizedIntent event
503
                        // get the value and promote to turn.recognized, topintent,topscore and lastintent
504
                        var recognizedResult = actionContext.State.GetValue<RecognizerResult>($"{TurnPath.DialogEvent}.value");
1✔
505

506
                        // #3572 set these here too (Even though the emitter may have set them) because this event can be emitted by declarative code.
507
                        var (name, score) = recognizedResult.GetTopScoringIntent();
1✔
508
                        actionContext.State.SetValue(TurnPath.Recognized, recognizedResult);
1✔
509
                        actionContext.State.SetValue(TurnPath.TopIntent, name);
1✔
510
                        actionContext.State.SetValue(TurnPath.TopScore, score);
1✔
511
                        actionContext.State.SetValue(DialogPath.LastIntent, name);
1✔
512

513
                        // process entities for ambiguity processing (We do this regardless of who handles the event)
514
                        ProcessEntities(actionContext, activity);
1✔
515
                        break;
1✔
516
                    }
517

518
                case AdaptiveEvents.ActivityReceived:
519
                    {
520
                        // We received an ActivityReceived event, promote the activity into turn.activity
521
                        actionContext.State.SetValue(TurnPath.Activity, dialogEvent.Value);
1✔
522
                        activity = ObjectPath.GetPathValue<Activity>(dialogEvent, "Value");
1✔
523
                        break;
524
                    }
525
            }
526

527
            EnsureDependenciesInstalled();
1✔
528

529
            // Count of events processed
530
            var count = actionContext.State.GetValue<uint>(DialogPath.EventCounter);
1✔
531
            actionContext.State.SetValue(DialogPath.EventCounter, ++count);
1✔
532

533
            // Look for triggered evt
534
            var handled = await QueueFirstMatchAsync(actionContext, dialogEvent, cancellationToken).ConfigureAwait(false);
1✔
535

536
            if (handled)
1✔
537
            {
538
                return true;
1✔
539
            }
540

541
            // Default processing
542
            if (preBubble)
1✔
543
            {
544
                switch (dialogEvent.Name)
1✔
545
                {
546
                    case AdaptiveEvents.BeginDialog:
547
                        if (actionContext.State.GetBoolValue(TurnPath.ActivityProcessed) == false)
1✔
548
                        {
549
                            // Emit leading ActivityReceived event
550
                            var activityReceivedEvent = new DialogEvent()
1✔
551
                            {
1✔
552
                                Name = AdaptiveEvents.ActivityReceived,
1✔
553
                                Value = actionContext.Context.Activity,
1✔
554
                                Bubble = false
1✔
555
                            };
1✔
556

557
                            handled = await ProcessEventAsync(actionContext, dialogEvent: activityReceivedEvent, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
558
                        }
559

560
                        break;
1✔
561

562
                    case AdaptiveEvents.ActivityReceived:
563
                        if (activity.Type == ActivityTypes.Message)
1✔
564
                        {
565
                            // Recognize utterance (ignore handled)
566
                            var recognizeUtteranceEvent = new DialogEvent
1✔
567
                            {
1✔
568
                                Name = AdaptiveEvents.RecognizeUtterance,
1✔
569
                                Value = activity,
1✔
570
                                Bubble = false
1✔
571
                            };
1✔
572
                            await ProcessEventAsync(actionContext, dialogEvent: recognizeUtteranceEvent, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
573

574
                            // Emit leading RecognizedIntent event
575
                            var recognized = actionContext.State.GetValue<RecognizerResult>(TurnPath.Recognized);
1✔
576
                            var recognizedIntentEvent = new DialogEvent
1✔
577
                            {
1✔
578
                                Name = AdaptiveEvents.RecognizedIntent,
1✔
579
                                Value = recognized,
1✔
580
                                Bubble = false
1✔
581
                            };
1✔
582
                            handled = await ProcessEventAsync(actionContext, dialogEvent: recognizedIntentEvent, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
583
                        }
584

585
                        // Has an interruption occured?
586
                        // - Setting this value to true causes any running inputs to re-prompt when they're
587
                        //   continued.  The developer can clear this flag if they want the input to instead
588
                        //   process the users uterrance when its continued.
589
                        if (handled)
1✔
590
                        {
591
                            actionContext.State.SetValue(TurnPath.Interrupted, true);
1✔
592
                        }
593

594
                        break;
1✔
595

596
                    case AdaptiveEvents.RecognizeUtterance:
597
                        {
598
                            if (activity.Type == ActivityTypes.Message)
1✔
599
                            {
600
                                // Recognize utterance
601
                                var recognizedResult = await OnRecognizeAsync(actionContext, activity, cancellationToken).ConfigureAwait(false);
1✔
602

603
                                // TODO figure out way to not use turn state to pass this value back to caller.
604
                                actionContext.State.SetValue(TurnPath.Recognized, recognizedResult);
1✔
605

606
                                // Bug #3572 set these here, because if allowedInterruption is true then event is not emitted, but folks still want the value.
607
                                var (name, score) = recognizedResult.GetTopScoringIntent();
1✔
608
                                actionContext.State.SetValue(TurnPath.TopIntent, name);
1✔
609
                                actionContext.State.SetValue(TurnPath.TopScore, score);
1✔
610
                                actionContext.State.SetValue(DialogPath.LastIntent, name);
1✔
611

612
                                if (Recognizer != null)
1✔
613
                                {
614
                                    await actionContext.DebuggerStepAsync(Recognizer, AdaptiveEvents.RecognizeUtterance, cancellationToken).ConfigureAwait(false);
1✔
615
                                }
616

617
                                handled = true;
1✔
618
                            }
619
                        }
620

621
                        break;
1✔
622

623
                    case AdaptiveEvents.RepromptDialog:
624
                        {
625
                            // AdaptiveDialogs handle new RepromptDialog as it gives access to the dialogContext.
626
                            await this.RepromptDialogAsync(actionContext, actionContext.ActiveDialog, cancellationToken).ConfigureAwait(false);
1✔
627
                            handled = true;
1✔
628
                        }
629

630
                        break;
1✔
631
                }
632
            }
633
            else
634
            {
635
                switch (dialogEvent.Name)
1✔
636
                {
637
                    case AdaptiveEvents.BeginDialog:
638
                        if (actionContext.State.GetBoolValue(TurnPath.ActivityProcessed) == false)
1✔
639
                        {
640
                            var activityReceivedEvent = new DialogEvent
1✔
641
                            {
1✔
642
                                Name = AdaptiveEvents.ActivityReceived,
1✔
643
                                Value = activity,
1✔
644
                                Bubble = false
1✔
645
                            };
1✔
646

647
                            handled = await ProcessEventAsync(actionContext, dialogEvent: activityReceivedEvent, preBubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
648
                        }
649

650
                        break;
1✔
651

652
                    case AdaptiveEvents.ActivityReceived:
653
                        if (activity.Type == ActivityTypes.Message)
1✔
654
                        {
655
                            // Empty sequence?
656
                            if (!actionContext.Actions.Any())
1✔
657
                            {
658
                                // Emit trailing unknownIntent event
659
                                var unknownIntentEvent = new DialogEvent
1✔
660
                                {
1✔
661
                                    Name = AdaptiveEvents.UnknownIntent,
1✔
662
                                    Bubble = false
1✔
663
                                };
1✔
664
                                handled = await ProcessEventAsync(actionContext, dialogEvent: unknownIntentEvent, preBubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
665
                            }
666
                            else
667
                            {
668
                                handled = false;
1✔
669
                            }
670
                        }
671

672
                        // Has an interruption occured?
673
                        // - Setting this value to true causes any running inputs to re-prompt when they're
674
                        //   continued.  The developer can clear this flag if they want the input to instead
675
                        //   process the users uterrance when its continued.
676
                        if (handled)
1✔
677
                        {
678
                            actionContext.State.SetValue(TurnPath.Interrupted, true);
1✔
679
                        }
680

681
                        break;
682
                }
683
            }
684

685
            return handled;
1✔
686
        }
1✔
687

688
        /// <summary>
689
        /// Waits for pending actions to complete and moves on to <see cref="OnEndOfActions"/>.
690
        /// </summary>
691
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
692
        /// <param name="options">Options used in evaluation. </param>
693
        /// <param name="cancellationToken">Optional, the <see cref="CancellationToken"/> that can be used by other objects or threads to receive notice of cancellation.</param>
694
        /// <returns>A <see cref="Task"/> representation of <see cref="DialogTurnResult"/>.</returns>
695
        protected async Task<DialogTurnResult> ContinueActionsAsync(DialogContext dc, object options, CancellationToken cancellationToken)
696
        {
697
            if (options is CancellationToken)
1✔
698
            {
699
                throw new ArgumentException("You cannot pass a cancellation token as options");
×
700
            }
701

702
            // Apply any queued up changes
703
            var actionContext = ToActionContext(dc);
1✔
704
            await actionContext.ApplyChangesAsync(cancellationToken).ConfigureAwait(false);
1✔
705

706
            // Get a unique instance ID for the current stack entry.
707
            // We need to do this because things like cancellation can cause us to be removed
708
            // from the stack and we want to detect this so we can stop processing actions.
709
            var instanceId = GetUniqueInstanceId(actionContext);
1✔
710

711
            // Initialize local interruption detection
712
            // - Any steps containing a dialog stack after the first step indicates the action was interrupted. We
713
            //   want to force a re-prompt and then end the turn when we encounter an interrupted step.
714
            var interrupted = false;
1✔
715

716
            // Execute queued actions
717
            var actionDC = CreateChildContext(actionContext);
1✔
718
            while (actionDC != null)
1✔
719
            {
720
                // DEBUG: To debug step execution set a breakpoint on line below and add a watch 
721
                //        statement for actionContext.Actions.
722
                DialogTurnResult result;
723
                if (actionDC.Stack.Count == 0)
1✔
724
                {
725
                    // Start step
726
                    var nextAction = actionContext.Actions.First();
1✔
727
                    result = await actionDC.BeginDialogAsync(nextAction.DialogId, nextAction.Options, cancellationToken).ConfigureAwait(false);
1✔
728
                }
729
                else
730
                {
731
                    // Set interrupted flag
732
                    if (interrupted && !actionDC.State.TryGetValue(TurnPath.Interrupted, out _))
1✔
733
                    {
734
                        actionDC.State.SetValue(TurnPath.Interrupted, true);
1✔
735
                    }
736

737
                    // Continue step execution
738
                    result = await actionDC.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
1✔
739
                }
740

741
                // Is the step waiting for input or were we cancelled?
742
                if (result.Status == DialogTurnStatus.Waiting || GetUniqueInstanceId(actionContext) != instanceId)
1✔
743
                {
744
                    return result;
1✔
745
                }
746

747
                // End current step
748
                await EndCurrentActionAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
749

750
                if (result.Status == DialogTurnStatus.CompleteAndWait)
1✔
751
                {
752
                    // Child dialog completed, but wants us to wait for a new activity
753
                    result.Status = DialogTurnStatus.Waiting;
1✔
754
                    return result;
1✔
755
                }
756

757
                var parentChanges = false;
1✔
758
                DialogContext root = actionContext;
1✔
759
                var parent = actionContext.Parent;
1✔
760
                while (parent != null)
1✔
761
                {
762
                    var ac = parent as ActionContext;
1✔
763
                    if (ac != null && ac.Changes != null && ac.Changes.Count > 0)
×
764
                    {
765
                        parentChanges = true;
×
766
                    }
767

768
                    root = parent;
1✔
769
                    parent = root.Parent;
1✔
770
                }
771

772
                // Execute next step
773
                if (parentChanges)
1✔
774
                {
775
                    // Recursively call ContinueDialogAsync() to apply parent changes and continue
776
                    // execution.
777
                    return await root.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
×
778
                }
779

780
                // Apply any local changes and fetch next action
781
                await actionContext.ApplyChangesAsync(cancellationToken).ConfigureAwait(false);
1✔
782
                actionDC = CreateChildContext(actionContext);
1✔
783
                interrupted = true;
1✔
784
            }
1✔
785

786
            return await OnEndOfActionsAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
787
        }
1✔
788

789
        /// <summary>
790
        /// OnSetScopedServices provides ability to set scoped services for the current dialogContext.
791
        /// </summary>
792
        /// <remarks>
793
        /// USe dialogContext.Services.Set(object) to set a scoped object that will be inherited by all children dialogContexts.
794
        /// </remarks>
795
        /// <param name="dialogContext">dialog Context.</param>
796
        protected virtual void OnSetScopedServices(DialogContext dialogContext)
797
        {
798
            if (Generator != null)
1✔
799
            {
800
                dialogContext.Services.Set(this.Generator);
1✔
801
            }
802
        }
1✔
803

804
        /// <summary>
805
        /// Removes the current most action from the given <see cref="ActionContext"/> if there are any.
806
        /// </summary>
807
        /// <param name="actionContext">The <see cref="ActionContext"/> for the current turn of conversation.</param>
808
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects.</param>
809
        /// <returns>A <see cref="Task"/> representing a boolean indicator for the result.</returns>
810
#pragma warning disable CA1801 // Review unused parameters (we can't remove the cancellationToken parameter withoutt breaking binary compat).
811
        protected Task<bool> EndCurrentActionAsync(ActionContext actionContext, CancellationToken cancellationToken = default)
812
#pragma warning restore CA1801 // Review unused parameters
813
        {
814
            if (actionContext.Actions.Any())
1✔
815
            {
816
                actionContext.Actions.RemoveAt(0);
1✔
817
            }
818

819
            return Task.FromResult(false);
1✔
820
        }
821

822
        /// <summary>
823
        /// Awaits for completed actions to finish processing entity assignments and finishes turn.
824
        /// </summary>
825
        /// <param name="actionContext">The <see cref="ActionContext"/> for the current turn of conversation.</param>
826
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects.</param>
827
        /// <returns>A <see cref="Task"/> representation of <see cref="DialogTurnResult"/>.</returns>
828
        protected async Task<DialogTurnResult> OnEndOfActionsAsync(ActionContext actionContext, CancellationToken cancellationToken = default)
829
        {
830
            // Is the current dialog still on the stack?
831
            if (actionContext.ActiveDialog != null)
1✔
832
            {
833
                // Completed actions so continue processing entity assignments
834
                var handled = await ProcessQueuesAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
835

836
                if (handled)
1✔
837
                {
838
                    // Still processing assignments
839
                    return await ContinueActionsAsync(actionContext, null, cancellationToken).ConfigureAwait(false);
1✔
840
                }
841
                else if (this.AutoEndDialog.GetValue(actionContext.State))
1✔
842
                {
843
                    actionContext.State.TryGetValue<object>(DefaultResultProperty, out var result);
1✔
844
                    return await actionContext.EndDialogAsync(result, cancellationToken).ConfigureAwait(false);
1✔
845
                }
846

847
                return EndOfTurn;
1✔
848
            }
849

850
            return new DialogTurnResult(DialogTurnStatus.Cancelled);
×
851
        }
1✔
852

853
        /// <summary>
854
        /// Recognizes intent for current activity given the class recognizer set, if set is null no intent will be recognized.
855
        /// </summary>
856
        /// <param name="actionContext">The <see cref="ActionContext"/> for the current turn of conversation.</param>
857
        /// <param name="activity"><see cref="Activity"/> to recognize.</param>
858
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects or threads to receive notice of cancellation.</param>
859
        /// <returns>A <see cref="Task"/> representing a <see cref="RecognizerResult"/>.</returns>
860
        protected async Task<RecognizerResult> OnRecognizeAsync(ActionContext actionContext, Activity activity, CancellationToken cancellationToken = default)
861
        {
862
            if (Recognizer != null)
1✔
863
            {
864
                lock (this.recognizerSet)
1✔
865
                {
866
                    if (!this.recognizerSet.Recognizers.Any())
1✔
867
                    {
868
                        this.recognizerSet.Recognizers.Add(this.Recognizer);
1✔
869
                        this.recognizerSet.Recognizers.Add(new ValueRecognizer());
1✔
870
                    }
871
                }
1✔
872

873
                var result = await recognizerSet.RecognizeAsync(actionContext, activity, cancellationToken).ConfigureAwait(false);
1✔
874

875
                if (result.Intents.Any())
1✔
876
                {
877
                    // Score
878
                    // Gathers all the intents with the highest Score value.
879
                    var scoreSorted = result.Intents.OrderByDescending(e => e.Value.Score).ToList();
1✔
880
                    var topIntents = scoreSorted.TakeWhile(e => e.Value.Score == scoreSorted[0].Value.Score).ToList();
1✔
881
                    
882
                    // Priority
883
                    // Gathers the Intent with the highest Priority (0 being the highest).
884
                    // Note: this functionality is based on the FirstSelector.SelectAsync method.
885
                    var topIntent = topIntents.FirstOrDefault();
1✔
886

887
                    if (topIntents.Count > 1)
1✔
888
                    {
889
                        var highestPriority = double.MaxValue;
1✔
890
                        foreach (var intent in topIntents)
1✔
891
                        {
892
                            var triggerIntent = Triggers.SingleOrDefault(x => x is OnIntent && (x as OnIntent).Intent == intent.Key);
1✔
893
                            var priority = triggerIntent.CurrentPriority(actionContext);
1✔
894
                            if (priority >= 0 && priority < highestPriority)
1✔
895
                            {
896
                                topIntent = intent;
1✔
897
                                highestPriority = priority;
1✔
898
                            }
899
                        }
900
                    }
901

902
                    result.Intents.Clear();
1✔
903
                    result.Intents.Add(topIntent);
1✔
904
                }
905
                else
906
                {
907
                    result.Intents.Add(NoneIntentKey, new IntentScore { Score = 0.0 });
×
908
                }
909

910
                return result;
1✔
911
            }
912

913
            // none intent if there is no recognizer
914
            return new RecognizerResult
1✔
915
            {
1✔
916
                Text = activity.Text ?? string.Empty,
1✔
917
                Intents = new Dictionary<string, IntentScore> { { NoneIntentKey, new IntentScore { Score = 0.0 } } },
1✔
918
            };
1✔
919
        }
1✔
920

921
        /// <summary>
922
        /// Ensures all dependencies for the class are installed.
923
        /// </summary>
924
        protected virtual void EnsureDependenciesInstalled()
925
        {
926
            if (!installedDependencies)
1✔
927
            {
928
                lock (this.syncLock)
1✔
929
                {
930
                    if (!installedDependencies)
1✔
931
                    {
932
                        installedDependencies = true;
1✔
933

934
                        var id = 0;
1✔
935
                        foreach (var trigger in Triggers)
1✔
936
                        {
937
                            if (trigger is IDialogDependencies depends)
1✔
938
                            {
939
                                foreach (var dlg in depends.GetDependencies())
1✔
940
                                {
941
                                    Dialogs.Add(dlg);
1✔
942
                                }
943
                            }
944

945
                            if (trigger.RunOnce)
1✔
946
                            {
947
                                needsTracker = true;
1✔
948
                            }
949

950
                            if (trigger.Priority == null)
1✔
951
                            {
952
                                // Constant expression defined from order
953
                                trigger.Priority = id;
×
954
                            }
955

956
                            if (trigger.Id == null)
1✔
957
                            {
958
                                trigger.Id = id++.ToString(CultureInfo.InvariantCulture);
1✔
959
                            }
960
                        }
961

962
                        // Wire up selector
963
                        if (Selector == null)
1✔
964
                        {
965
                            // Default to most specific then first
966
                            Selector = new MostSpecificSelector { Selector = new FirstSelector() };
1✔
967
                        }
968

969
                        this.Selector.Initialize(Triggers, evaluate: true);
1✔
970
                    }
971
                }
1✔
972
            }
973
        }
1✔
974

975
        // This function goes through the entity assignments and emits events if present.
976
        private async Task<bool> ProcessQueuesAsync(ActionContext actionContext, CancellationToken cancellationToken)
977
        {
978
            DialogEvent evt;
979
            bool handled;
980
            var assignments = EntityAssignments.Read(actionContext);
1✔
981
            var nextAssignment = assignments.NextAssignment();
1✔
982
            if (nextAssignment != null)
1✔
983
            {
984
                object val = nextAssignment;
1✔
985
                if (nextAssignment.Alternative != null)
1✔
986
                {
987
                    val = nextAssignment.Alternatives.ToList();
1✔
988
                }
989

990
                if (nextAssignment.RaisedCount++ == 0)
1✔
991
                {
992
                    // Reset retries when new form event is first issued
993
                    actionContext.State.RemoveValue(DialogPath.Retries);
1✔
994
                }
995

996
                evt = new DialogEvent() { Name = nextAssignment.Event, Value = val, Bubble = false };
1✔
997
                if (nextAssignment.Event == AdaptiveEvents.AssignEntity)
1✔
998
                {
999
                    // TODO: For now, I'm going to dereference to a one-level array value.  There is a bug in the current code in the distinction between
1000
                    // @ which is supposed to unwrap down to non-array and @@ which returns the whole thing. @ in the curent code works by doing [0] which
1001
                    // is not enough.
1002
                    var entity = nextAssignment.Value.Value;
1✔
1003
                    if (!(entity is JArray))
1✔
1004
                    {
1005
                        entity = new object[] { entity };
1✔
1006
                    }
1007

1008
                    actionContext.State.SetValue($"{TurnPath.Recognized}.entities.{nextAssignment.Value.Name}", entity);
1✔
1009
                    assignments.Dequeue(actionContext);
1✔
1010
                }
1011

1012
                actionContext.State.SetValue(DialogPath.LastEvent, evt.Name);
1✔
1013
                handled = await this.ProcessEventAsync(actionContext, dialogEvent: evt, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
1014
                if (!handled)
1✔
1015
                {
1016
                    // If event wasn't handled, remove it
1017
                    if (nextAssignment != null && nextAssignment.Event != AdaptiveEvents.AssignEntity)
×
1018
                    {
1019
                        assignments.Dequeue(actionContext);
×
1020
                    }
1021

1022
                    // See if more assignements or end of actions
1023
                    handled = await this.ProcessQueuesAsync(actionContext, cancellationToken).ConfigureAwait(false);
×
1024
                }
1025
            }
1026
            else
1027
            {
1028
                // Emit end of actions
1029
                evt = new DialogEvent() { Name = AdaptiveEvents.EndOfActions, Bubble = false };
1✔
1030
                actionContext.State.SetValue(DialogPath.LastEvent, evt.Name);
1✔
1031
                handled = await this.ProcessEventAsync(actionContext, dialogEvent: evt, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
1032
            }
1033

1034
            return handled;
1✔
1035
        }
1✔
1036

1037
        private string GetUniqueInstanceId(DialogContext dc)
1038
        {
1039
            return dc.Stack.Count > 0 ? $"{dc.Stack.Count}:{dc.ActiveDialog.Id}" : string.Empty;
1✔
1040
        }
1041

1042
        private async Task<bool> QueueFirstMatchAsync(ActionContext actionContext, DialogEvent dialogEvent, CancellationToken cancellationToken)
1043
        {
1044
            var selection = await Selector.SelectAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
1045
            if (selection.Any())
1✔
1046
            {
1047
                var condition = selection[0];
1✔
1048
                await actionContext.DebuggerStepAsync(condition, dialogEvent, cancellationToken).ConfigureAwait(false);
1✔
1049
                System.Diagnostics.Trace.TraceInformation($"Executing Dialog: {Id} Rule[{condition.Id}]: {condition.GetType().Name}: {condition.GetExpression()}");
1✔
1050

1051
                var properties = new Dictionary<string, string>()
1✔
1052
                {
1✔
1053
                    { "DialogId", Id },
1✔
1054
                    { "Expression", condition.GetExpression().ToString() },
1✔
1055
                    { "Kind", $"Microsoft.{condition.GetType().Name}" },
1✔
1056
                    { "ConditionId", condition.Id },
1✔
1057
                    { "context", TelemetryLoggerConstants.TriggerEvent }
1✔
1058
                };
1✔
1059
                TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
1060

1061
                var changes = await condition.ExecuteAsync(actionContext).ConfigureAwait(false);
1✔
1062

1063
                if (changes != null && changes.Any())
1✔
1064
                {
1065
                    actionContext.QueueChanges(changes[0]);
1✔
1066
                    return true;
1✔
1067
                }
1068
            }
×
1069

1070
            return false;
1✔
1071
        }
1✔
1072

1073
        private ActionContext ToActionContext(DialogContext dc)
1074
        {
1075
            var activeDialogState = dc.ActiveDialog.State as Dictionary<string, object>;
1✔
1076
            var state = activeDialogState[AdaptiveKey] as AdaptiveDialogState;
1✔
1077

1078
            if (state == null)
1✔
1079
            {
1080
                state = new AdaptiveDialogState();
×
1081
                activeDialogState[AdaptiveKey] = state;
×
1082
            }
1083

1084
            if (state.Actions == null)
1✔
1085
            {
1086
                state.Actions = new List<ActionState>();
×
1087
            }
1088

1089
            var actionContext = new ActionContext(dc.Dialogs, dc, new DialogState { DialogStack = dc.Stack }, state.Actions, changeTurnKey);
1✔
1090
            actionContext.Parent = dc.Parent;
1✔
1091
            return actionContext;
1✔
1092
        }
1093

1094
        // Process entities to identify ambiguity and possible assigment to properties.  Broadly the steps are:
1095
        // Normalize entities to include meta-data
1096
        // Check to see if an entity is in response to a previous ambiguity event
1097
        // Assign entities to possible properties
1098
        // Merge new assignments into existing assignments of ambiguity events
1099
        private void ProcessEntities(ActionContext actionContext, Activity activity)
1100
        {
1101
            if (dialogSchema != null)
1✔
1102
            {
1103
                if (actionContext.State.TryGetValue<string>(DialogPath.LastEvent, out var lastEvent))
1✔
1104
                {
1105
                    actionContext.State.RemoveValue(DialogPath.LastEvent);
1✔
1106
                }
1107

1108
                var assignments = EntityAssignments.Read(actionContext);
1✔
1109
                var entities = NormalizeEntities(actionContext);
1✔
1110
                var utterance = activity?.AsMessageActivity()?.Text;
×
1111

1112
                // Utterance is a special entity that corresponds to the full utterance
1113
                entities[UtteranceKey] = new List<EntityInfo>
1✔
1114
                {
1✔
1115
                    new EntityInfo { Priority = float.MaxValue, Coverage = 1.0, Start = 0, End = utterance.Length, Name = UtteranceKey, Score = 0.0, Type = "string", Value = utterance, Text = utterance }
1✔
1116
                };
1✔
1117
                var recognized = AssignEntities(actionContext, entities, assignments, lastEvent);
1✔
1118
                var unrecognized = SplitUtterance(utterance, recognized);
1✔
1119

1120
                actionContext.State.SetValue(TurnPath.UnrecognizedText, unrecognized);
1✔
1121
                actionContext.State.SetValue(TurnPath.RecognizedEntities, recognized);
1✔
1122
                assignments.Write(actionContext);
1✔
1123
            }
1124
        }
1✔
1125

1126
        // Split an utterance into unrecognized parts of text
1127
        private List<string> SplitUtterance(string utterance, List<EntityInfo> recognized)
1128
        {
1129
            var unrecognized = new List<string>();
1✔
1130
            var current = 0;
1✔
1131
            foreach (var entity in recognized)
1✔
1132
            {
1133
                if (entity.Start > current)
1✔
1134
                {
1135
                    unrecognized.Add(utterance.Substring(current, entity.Start - current).Trim());
1✔
1136
                }
1137

1138
                current = entity.End;
1✔
1139
            }
1140

1141
            if (current < utterance.Length)
1✔
1142
            {
1143
                unrecognized.Add(utterance.Substring(current));
1✔
1144
            }
1145

1146
            return unrecognized;
1✔
1147
        }
1148

1149
        // Expand object that contains entities which can be op, property or leaf entity
1150
        private void ExpandEntityObject(
1151
            JObject entities, string op, string property, JObject rootInstance, List<string> operations, List<string> properties, uint turn, string text, Dictionary<string, List<EntityInfo>> entityToInfo)
1152
        {
1153
            foreach (var token in entities)
1✔
1154
            {
1155
                var entityName = token.Key;
1✔
1156
                var instances = entities[InstanceKey][entityName] as JArray;
1✔
1157
                ExpandEntities(entityName, token.Value as JArray, instances, rootInstance, op, property, operations, properties, turn, text, entityToInfo);
1✔
1158
            }
1159
        }
1✔
1160

1161
        private string StripProperty(string name)
1162
            => name.EndsWith(PropertyEnding, StringComparison.InvariantCulture) ? name.Substring(0, name.Length - PropertyEnding.Length) : name;
1✔
1163

1164
        // Expand the array of entities for a particular entity
1165
        private void ExpandEntities(
1166
            string name, JArray entities, JArray instances, JObject rootInstance, string op, string property, List<string> operations, List<string> properties, uint turn, string text, Dictionary<string, List<EntityInfo>> entityToInfo)
1167
        {
1168
            if (!name.StartsWith("$", StringComparison.InvariantCulture))
1✔
1169
            {
1170
                // Entities representing schema properties end in "Property" to prevent name collisions with the property itself.
1171
                var propName = StripProperty(name);
1✔
1172
                string entityName = null;
1✔
1173
                var isOp = false;
1✔
1174
                var isProperty = false;
1✔
1175
                if (operations.Contains(name))
1✔
1176
                {
1177
                    op = name;
1✔
1178
                    isOp = true;
1✔
1179
                }
1180
                else if (properties.Contains(propName))
1✔
1181
                {
1182
                    property = propName;
1✔
1183
                    isProperty = true;
1✔
1184
                }
1185
                else
1186
                {
1187
                    entityName = name;
1✔
1188
                }
1189

1190
                for (var entityIndex = 0; entityIndex < entities.Count; ++entityIndex)
1✔
1191
                {
1192
                    var entity = entities[entityIndex];
1✔
1193
                    var instance = instances[entityIndex] as JObject;
1✔
1194
                    var root = rootInstance;
1✔
1195
                    if (root == null)
1✔
1196
                    {
1197
                        // Keep the root entity name and position to help with overlap
1198
                        root = instance.DeepClone() as JObject;
1✔
1199
                        root["type"] = $"{name}{entityIndex}";
1✔
1200
                    }
1201

1202
                    if (entityName != null)
1✔
1203
                    {
1204
                        ExpandEntity(entityName, entity, instance, root, op, property, turn, text, entityToInfo);
1✔
1205
                    }
1206
                    else if (entity is JObject entityObject)
1✔
1207
                    {
1208
                        if (entityObject.Count == 0)
1✔
1209
                        {
1210
                            if (isOp)
1✔
1211
                            {
1212
                                // Handle operator with no children
1213
                                ExpandEntity(op, null, instance, root, op, property, turn, text, entityToInfo);
×
1214
                            }
1215
                            else if (isProperty)
1✔
1216
                            {
1217
                                // Handle property with no children
1218
                                ExpandEntity(property, null, instance, root, op, property, turn, text, entityToInfo);
1✔
1219
                            }
1220
                        }
1221
                        else
1222
                        {
1223
                            ExpandEntityObject(entityObject, op, property, root, operations, properties, turn, text, entityToInfo);
1✔
1224
                        }
1225
                    }
1226
                    else if (isOp)
×
1227
                    {
1228
                        // Handle global operator with no children in model
1229
                        ExpandEntity(op, null, instance, root, op, property, turn, text, entityToInfo);
×
1230
                    }
1231
                }
1232
            }
1233
        }
1✔
1234

1235
        // Expand a leaf entity into EntityInfo.
1236
        private void ExpandEntity(string name, object value, dynamic instance, dynamic rootInstance, string op, string property, uint turn, string text, Dictionary<string, List<EntityInfo>> entityToInfo)
1237
        {
1238
            if (instance != null && rootInstance != null)
×
1239
            {
1240
                if (!entityToInfo.TryGetValue(name, out List<EntityInfo> infos))
1✔
1241
                {
1242
                    infos = new List<EntityInfo>();
1✔
1243
                    entityToInfo[name] = infos;
1✔
1244
                }
1245

1246
                var info = new EntityInfo
×
1247
                {
×
1248
                    WhenRecognized = turn,
×
1249
                    Name = name,
×
1250
                    Value = value,
×
1251
                    Operation = op,
×
1252
                    Property = property,
×
1253
                    Start = (int)rootInstance.startIndex,
×
1254
                    End = (int)rootInstance.endIndex,
×
1255
                    RootEntity = rootInstance.type,
×
1256
                    Text = (string)(rootInstance.text ?? string.Empty),
×
1257
                    Type = (string)(instance.type ?? null),
×
1258
                    Score = (double)(instance.score ?? 0.0d),
×
1259
                    Priority = 0,
×
1260
                };
×
1261

1262
                info.Coverage = (info.End - info.Start) / (double)text.Length;
1✔
1263
                infos.Add(info);
1✔
1264
            }
1265
        }
1✔
1266

1267
        // Combine entity values and $instance meta-data and expand out op/property
1268
        // Structure of entities.  
1269
        //{
1270
        //  "<op>": [
1271
        //    // Op property
1272
        //    {
1273
        //      "<property>": [
1274
        //        // Property without entities
1275
        //        {},
1276
        //        // Property with entities
1277
        //        {
1278
        //          "<entity>": [],
1279
        //          "$instance": []
1280
        //        }
1281
        //      ],
1282
        //      "$instance": []
1283
        //    },
1284
        //    // Op entity
1285
        //    {
1286
        //    "<entity> ": [],
1287
        //      "$instance": []
1288
        //    }
1289
        //  ],
1290
        //  // Direct property
1291
        //  "<property>": [
1292
        //    {},
1293
        //    {
1294
        //    "<entity>": [],
1295
        //      "$instance": []
1296
        //    }
1297
        //  ],
1298
        //  // Direct entity
1299
        //  "<entity>": [],
1300
        //  "$instance": []
1301
        //}
1302
        private Dictionary<string, List<EntityInfo>> NormalizeEntities(ActionContext actionContext)
1303
        {
1304
            var entityToInfo = new Dictionary<string, List<EntityInfo>>();
1✔
1305
            var text = actionContext.State.GetValue<string>(TurnPath.Recognized + ".text");
1✔
1306
            if (actionContext.State.TryGetValue<dynamic>(TurnPath.Recognized + ".entities", out var entities))
1✔
1307
            {
1308
                var turn = actionContext.State.GetValue<uint>(DialogPath.EventCounter);
1✔
1309
                var operations = dialogSchema.Schema[OperationsKey]?.ToObject<List<string>>() ?? new List<string>();
×
1310
                var properties = dialogSchema.Property.Children.Select((prop) => prop.Name).ToList<string>();
1✔
1311
                ExpandEntityObject(entities, null, null, null, operations, properties, turn, text, entityToInfo);
1✔
1312
            }
1313

1314
            // When there are multiple possible resolutions for the same entity that overlap, pick the one that covers the
1315
            // most of the utterance.
1316
            foreach (var infos in entityToInfo.Values)
1✔
1317
            {
1318
                infos.Sort((entity1, entity2) =>
1✔
1319
                {
1✔
1320
                    var val = 0;
1✔
1321
                    if (entity1.Start == entity2.Start)
1✔
1322
                    {
1✔
1323
                        if (entity1.End > entity2.End)
1✔
1324
                        {
1✔
1325
                            val = -1;
×
1326
                        }
1✔
1327
                        else if (entity1.End < entity2.End)
1✔
1328
                        {
1✔
1329
                            val = +1;
×
1330
                        }
1✔
1331
                    }
1✔
1332
                    else if (entity1.Start < entity2.Start)
1✔
1333
                    {
1✔
1334
                        val = -1;
1✔
1335
                    }
1✔
1336
                    else
1✔
1337
                    {
1✔
1338
                        val = +1;
×
1339
                    }
1✔
1340

1✔
1341
                    return val;
1✔
1342
                });
1✔
1343
                for (var i = 0; i < infos.Count; ++i)
1✔
1344
                {
1345
                    var current = infos[i];
1✔
1346
                    for (var j = i + 1; j < infos.Count;)
1✔
1347
                    {
1348
                        var alt = infos[j];
1✔
1349
                        if (current.Covers(alt))
1✔
1350
                        {
1351
                            _ = infos.Remove(alt);
1✔
1352
                        }
1353
                        else
1354
                        {
1355
                            ++j;
1✔
1356
                        }
1357
                    }
1358
                }
1359
            }
1360

1361
            return entityToInfo;
1✔
1362
        }
1363

1364
        // An entity matches an assignment if the detected operation/property match
1365
        private bool MatchesAssignment(EntityInfo entity, EntityAssignment assignment)
1366
         => (entity.Operation == null || entity.Operation == assignment.Operation)
×
1367
            && (entity.Property == null || entity.Property == assignment.Property);
×
1368

1369
        // Generate candidate assignments including property and operation
1370
        private IEnumerable<EntityAssignment> Candidates(Dictionary<string, List<EntityInfo>> entities, string[] expected, string lastEvent, EntityAssignment nextAssignment, JObject askDefault, JObject dialogDefault)
1371
        {
1372
            var globalExpectedOnly = dialogSchema.Schema[ExpectedOnlyKey]?.ToObject<List<string>>() ?? new List<string>();
1✔
1373
            var requiresValue = dialogSchema.Schema[RequiresValueKey]?.ToObject<List<string>>() ?? new List<string>();
1✔
1374
            var assignments = new List<EntityAssignment>();
1✔
1375

1376
            // Add entities with a recognized property
1377
            foreach (var alternatives in entities.Values)
1✔
1378
            {
1379
                foreach (var alternative in alternatives)
1✔
1380
                {
1381
                    if (alternative.Property != null && (alternative.Value != null || !requiresValue.Contains(alternative.Operation)))
1✔
1382
                    {
1383
                        assignments.Add(new EntityAssignment
1✔
1384
                        {
1✔
1385
                            Value = alternative,
1✔
1386
                            Property = alternative.Property,
1✔
1387
                            Operation = alternative.Operation,
1✔
1388
                            IsExpected = expected.Contains(alternative.Property)
1✔
1389
                        });
1✔
1390
                    }
1391
                }
1392
            }
1393

1394
            // Find possible mappings for entities without a property or where property entities are expected
1395
            foreach (var propSchema in dialogSchema.Property.Children)
1✔
1396
            {
1397
                var isExpected = expected.Contains(propSchema.Name);
1✔
1398
                var expectedOnly = propSchema.ExpectedOnly ?? globalExpectedOnly;
1✔
1399
                foreach (var propEntity in propSchema.Entities)
1✔
1400
                {
1401
                    var entityName = StripProperty(propEntity);
1✔
1402
                    if (entities.TryGetValue(entityName, out var matches) && (isExpected || !expectedOnly.Contains(entityName)))
1✔
1403
                    {
1404
                        foreach (var entity in matches)
1✔
1405
                        {
1406
                            if (entity.Property == null)
1✔
1407
                            {
1408
                                assignments.Add(new EntityAssignment
1✔
1409
                                {
1✔
1410
                                    Value = entity,
1✔
1411
                                    Property = propSchema.Name,
1✔
1412
                                    Operation = entity.Operation,
1✔
1413
                                    IsExpected = isExpected
1✔
1414
                                });
1✔
1415
                            }
1416
                            else if (entity.Property == entityName && entity.Value == null && entity.Operation == null && isExpected)
1✔
1417
                            {
1418
                                // Recast property with no value as match for property entities
1419
                                assignments.Add(new EntityAssignment
1✔
1420
                                {
1✔
1421
                                    Value = entity,
1✔
1422
                                    Property = propSchema.Name,
1✔
1423
                                    Operation = null,
1✔
1424
                                    IsExpected = isExpected,
1✔
1425
                                });
1✔
1426
                            }
1427
                        }
1428
                    }
1429
                }
1430
            }
1431

1432
            // Add default operations
1433
            foreach (var assignment in assignments)
1✔
1434
            {
1435
                if (assignment.Operation == null)
1✔
1436
                {
1437
                    // Assign missing operation
1438
                    if (lastEvent == AdaptiveEvents.ChooseEntity
1✔
1439
                        && assignment.Value.Property == nextAssignment.Property)
1✔
1440
                    {
1441
                        // Property and value match ambiguous entity
1442
                        assignment.Operation = AdaptiveEvents.ChooseEntity;
1✔
1443
                        assignment.IsExpected = true;
1✔
1444
                    }
1445
                    else
1446
                    {
1447
                        // Assign default operator
1448
                        assignment.Operation = DefaultOperation(assignment, askDefault, dialogDefault);
1✔
1449
                    }
1450
                }
1451
            }
1452

1453
            // Add choose property matches
1454
            if (lastEvent == AdaptiveEvents.ChooseProperty)
1✔
1455
            {
1456
                foreach (var alternatives in entities.Values)
1✔
1457
                {
1458
                    foreach (var alternative in alternatives)
1✔
1459
                    {
1460
                        if (alternative.Value == null)
1✔
1461
                        {
1462
                            // If alternative matches one alternative it answers chooseProperty
1463
                            var matches = nextAssignment.Alternatives.Where(a => MatchesAssignment(alternative, a));
1✔
1464
                            if (matches.Count() == 1)
1✔
1465
                            {
1466
                                assignments.Add(new EntityAssignment
1✔
1467
                                {
1✔
1468
                                    Value = alternative,
1✔
1469
                                    Operation = AdaptiveEvents.ChooseProperty,
1✔
1470
                                    IsExpected = true
1✔
1471
                                });
1✔
1472
                            }
1473
                        }
1474
                    }
1475
                }
1476
            }
1477

1478
            // Add pure operations
1479
            foreach (var alternatives in entities.Values)
1✔
1480
            {
1481
                foreach (var alternative in alternatives)
1✔
1482
                {
1483
                    if (alternative.Operation != null && alternative.Property == null && alternative.Value == null)
×
1484
                    {
1485
                        var assignment = new EntityAssignment
×
1486
                        {
×
1487
                            Value = alternative,
×
1488
                            Property = null,
×
1489
                            Operation = alternative.Operation,
×
1490
                            IsExpected = false
×
1491
                        };
×
1492
                        assignments.Add(assignment);
×
1493
                    }
1494
                }
1495
            }
1496

1497
            // Preserve expectedProperties if there is no property
1498
            foreach (var assignment in assignments)
1✔
1499
            {
1500
                if (assignment.Property == null)
1✔
1501
                {
1502
                    assignment.ExpectedProperties = expected.ToList();
1✔
1503
                }
1504
            }
1505

1506
            return assignments;
1✔
1507
        }
1508

1509
        private void AddAssignment(EntityAssignment assignment, EntityAssignments assignments)
1510
        {
1511
            // Entities without a property or operation are available as entities only when found
1512
            if (assignment.Property != null || assignment.Operation != null)
×
1513
            {
1514
                if (assignment.Alternative != null)
1✔
1515
                {
1516
                    assignment.Event = AdaptiveEvents.ChooseProperty;
1✔
1517
                }
1518
                else if (assignment.Value.Value is JArray arr)
1✔
1519
                {
1520
                    if (arr.Count > 1)
1✔
1521
                    {
1522
                        assignment.Event = AdaptiveEvents.ChooseEntity;
1✔
1523
                    }
1524
                    else
1525
                    {
1526
                        assignment.Event = AdaptiveEvents.AssignEntity;
1✔
1527
                        assignment.Value.Value = arr[0];
1✔
1528
                    }
1529
                }
1530
                else
1531
                {
1532
                    assignment.Event = AdaptiveEvents.AssignEntity;
1✔
1533
                }
1534

1535
                assignments.Assignments.Add(assignment);
1✔
1536
            }
1537
        }
1✔
1538

1539
        // Have each property pick which overlapping entity is the best one
1540
        // This can happen because LUIS will return both 'wheat' and 'whole wheat' as the same list entity.
1541
        private IEnumerable<EntityAssignment> RemoveOverlappingPerProperty(IEnumerable<EntityAssignment> candidates)
1542
        {
1543
            var perProperty = from candidate in candidates
1✔
1544
                              group candidate by candidate.Property;
1✔
1545
            foreach (var propChoices in perProperty)
1✔
1546
            {
1547
                var entityPreferences = dialogSchema.PathToSchema(propChoices.Key).Entities;
1✔
1548
                var choices = propChoices.ToList();
1✔
1549

1550
                // Assume preference by order listed in mappings
1551
                // Alternatives would be to look at coverage or other metrics
1552
                foreach (var entity in entityPreferences)
1✔
1553
                {
1554
                    EntityAssignment candidate;
1555
                    do
1556
                    {
1557
                        candidate = null;
1✔
1558
                        foreach (var mapping in choices)
1✔
1559
                        {
1560
                            if (mapping.Value.Name == entity)
1✔
1561
                            {
1562
                                candidate = mapping;
1✔
1563
                                break;
1✔
1564
                            }
1565
                        }
1566

1567
                        if (candidate != null)
1✔
1568
                        {
1569
                            // Remove any overlapping entities without a common root
1570
                            choices.RemoveAll(choice => choice == candidate || (!choice.Value.SharesRoot(candidate.Value) && choice.Value.Overlaps(candidate.Value)));
1✔
1571
                            yield return candidate;
1✔
1572
                        }
1573
                    }
1574
                    while (candidate != null);
1✔
1575
                }
1✔
1576

1577
                // Keep remaining properties for things like show/clear that are not property specific
1578
                foreach (var choice in choices)
1✔
1579
                {
1580
                    yield return choice;
1✔
1581
                }
1582
            }
1✔
1583
        }
1✔
1584

1585
        // Return the default operation for an assignment by looking at the per-ask and dialog defaults
1586
        private string DefaultOperation(EntityAssignment assignment, JObject askDefault, JObject dialogDefault)
1587
        {
1588
            string operation = null;
1✔
1589
            if (assignment.Property != null)
1✔
1590
            {
1591
                if (askDefault != null && (askDefault.TryGetValue(assignment.Value.Name, out var askOp) || askDefault.TryGetValue(string.Empty, out askOp)))
×
1592
                {
1593
                    operation = askOp.Value<string>();
×
1594
                }
1595
                else if (dialogDefault != null
1✔
1596
                        && (dialogDefault.TryGetValue(assignment.Property, out var entities)
1✔
1597
                            || dialogDefault.TryGetValue(string.Empty, out entities))
1✔
1598
                        && ((entities as JObject).TryGetValue(assignment.Value.Name, out var dialogOp)
1✔
1599
                            || (entities as JObject).TryGetValue(string.Empty, out dialogOp)))
1✔
1600
                {
1601
                    operation = dialogOp.Value<string>();
1✔
1602
                }
1603
            }
1604

1605
            return operation;
1✔
1606
        }
1607

1608
        // Choose between competing interpretations
1609
        // This works by:
1610
        // * Generate candidate assignments including inferred property and operator if missing
1611
        // * Order by expected, then default operation to prefer expected things
1612
        // * Pick a candidate, identify alternatives and remove from pool of candidates
1613
        // * Alternatives overlap and are filtered by non-default op and biggest interpretation containing alternative 
1614
        // * The new assignments are then ordered by recency and phrase order and merged with existing assignments
1615
        private List<EntityInfo> AssignEntities(ActionContext actionContext, Dictionary<string, List<EntityInfo>> entities, EntityAssignments existing, string lastEvent)
1616
        {
1617
            var assignments = new EntityAssignments();
1✔
1618
            if (!actionContext.State.TryGetValue<string[]>(DialogPath.ExpectedProperties, out var expected))
1✔
1619
            {
1620
                expected = Array.Empty<string>();
1✔
1621
            }
1622

1623
            // default op from the last Ask action.
1624
            var askDefaultOp = actionContext.State.GetValue<JObject>(DialogPath.DefaultOperation);
1✔
1625

1626
            // default operation from the current adaptive dialog.
1627
            var defaultOp = dialogSchema.Schema[DefaultOperationKey]?.ToObject<JObject>();
×
1628

1629
            var nextAssignment = existing.NextAssignment();
1✔
1630
            var candidates = (from candidate in RemoveOverlappingPerProperty(Candidates(entities, expected, lastEvent, nextAssignment, askDefaultOp, defaultOp))
1✔
1631
                              orderby
1✔
1632
                                candidate.IsExpected descending,
1✔
1633
                                candidate.Operation == DefaultOperation(candidate, askDefaultOp, defaultOp) descending
1✔
1634
                              select candidate).ToList();
1✔
1635
            var usedEntities = new HashSet<EntityInfo>(from candidate in candidates select candidate.Value);
1✔
1636
            List<string> expectedChoices = null;
1✔
1637
            var choices = new List<EntityAssignment>();
1✔
1638
            while (candidates.Any())
1✔
1639
            {
1640
                var candidate = candidates.First();
1✔
1641

1642
                // Alternatives are either for the same entity or from different roots
1643
                var alternatives = (from alt in candidates
1✔
1644
                                    where candidate.Value.Overlaps(alt.Value) && (!candidate.Value.SharesRoot(alt.Value) || candidate.Value == alt.Value)
1✔
1645
                                    select alt).ToList();
1✔
1646
                candidates = candidates.Except(alternatives).ToList();
1✔
1647
                foreach (var alternative in alternatives)
1✔
1648
                {
1649
                    usedEntities.Add(alternative.Value);
1✔
1650
                }
1651

1652
                if (candidate.IsExpected && candidate.Value.Name != UtteranceKey)
1✔
1653
                {
1654
                    // If expected binds entity, drop unexpected alternatives unless they have an explicit operation
1655
                    alternatives.RemoveAll(a => !a.IsExpected && a.Value.Operation == null);
1✔
1656
                }
1657

1658
                // Find alternative that covers the largest amount of utterance
1659
                candidate = (from alternative in alternatives orderby alternative.Value.Name == UtteranceKey ? 0 : alternative.Value.End - alternative.Value.Start descending select alternative).First();
1✔
1660

1661
                // Remove all alternatives that are fully contained in largest
1662
                alternatives.RemoveAll(a => candidate.Value.Covers(a.Value));
1✔
1663

1664
                var mapped = false;
1✔
1665
                if (candidate.Operation == AdaptiveEvents.ChooseEntity)
1✔
1666
                {
1667
                    // Property has resolution so remove entity ambiguity
1668
                    var entityChoices = existing.Dequeue(actionContext);
1✔
1669
                    candidate.Operation = entityChoices.Operation;
1✔
1670
                    if (candidate.Value.Value is JArray values && values.Count > 1)
1✔
1671
                    {
1672
                        // Resolve ambiguous response to one of the original choices
1673
                        var originalChoices = entityChoices.Value.Value as JArray;
×
1674
                        var intersection = values.Intersect(originalChoices);
×
1675
                        if (intersection.Any())
×
1676
                        {
1677
                            candidate.Value.Value = intersection;
×
1678
                        }
1679
                    }
1680
                }
1681
                else if (candidate.Operation == AdaptiveEvents.ChooseProperty)
1✔
1682
                {
1683
                    choices = nextAssignment.Alternatives.ToList();
1✔
1684
                    var choice = choices.Find(a => MatchesAssignment(candidate.Value, a));
1✔
1685
                    if (choice != null)
1✔
1686
                    {
1687
                        // Resolve choice, pretend it was expected and add to assignments
1688
                        expectedChoices = new List<string>();
1✔
1689
                        choice.IsExpected = true;
1✔
1690
                        choice.Alternative = null;
1✔
1691
                        if (choice.Property != null)
1✔
1692
                        {
1693
                            expectedChoices.Add(choice.Property);
1✔
1694
                        }
1695
                        else if (choice.ExpectedProperties != null)
×
1696
                        {
1697
                            expectedChoices.AddRange(choice.ExpectedProperties);
×
1698
                        }
1699

1700
                        AddAssignment(choice, assignments);
1✔
1701
                        choices.RemoveAll(c => c.Value.Overlaps(choice.Value));
1✔
1702
                        mapped = true;
1✔
1703
                    }
1704
                }
1705

1706
                candidate.AddAlternatives(alternatives);
1✔
1707
                if (!mapped)
1✔
1708
                {
1709
                    AddAssignment(candidate, assignments);
1✔
1710
                }
1711
            }
1712

1713
            if (expectedChoices != null)
1✔
1714
            {
1715
                // When choosing between property assignments, make the assignments be expected.
1716
                if (expectedChoices.Any())
1✔
1717
                {
1718
                    actionContext.State.SetValue(DialogPath.ExpectedProperties, expectedChoices);
1✔
1719
                }
1720

1721
                // Add back in any non-overlapping choices that have not been resolved
1722
                while (choices.Any())
1✔
1723
                {
1724
                    var choice = choices.First();
×
1725
                    var overlaps = from alt in choices where choice.Value.Overlaps(alt.Value) select alt;
×
1726
                    choice.AddAlternatives(overlaps);
×
1727
                    AddAssignment(choice, assignments);
×
1728
                    choices.RemoveAll(c => c.Value.Overlaps(choice.Value));
×
1729
                }
1730

1731
                existing.Dequeue(actionContext);
1✔
1732
            }
1733

1734
            var operations = new EntityAssignmentComparer(dialogSchema.Schema[OperationsKey]?.ToObject<string[]>() ?? Array.Empty<string>());
×
1735
            MergeAssignments(assignments, existing, operations);
1✔
1736
            return usedEntities.ToList();
1✔
1737
        }
1738

1739
        // a replaces b when it refers to the same singleton property and is newer or later in same utterance and it is not a bare property
1740
        // -1 a replaces b
1741
        //  0 no replacement
1742
        // +1 b replaces a
1743
        private int Replaces(EntityAssignment a, EntityAssignment b)
1744
        {
1745
            var replaces = 0;
1✔
1746
            foreach (var aAlt in a.Alternatives)
1✔
1747
            {
1748
                foreach (var bAlt in b.Alternatives)
1✔
1749
                {
1750
                    if (aAlt.Property == bAlt.Property && aAlt.Value.Value != null && bAlt.Value.Value != null)
1✔
1751
                    {
1752
                        var prop = dialogSchema.PathToSchema(aAlt.Property);
1✔
1753
                        if (!prop.IsArray)
1✔
1754
                        {
1755
                            replaces = -aAlt.Value.WhenRecognized.CompareTo(bAlt.Value.WhenRecognized);
1✔
1756
                            if (replaces == 0)
1✔
1757
                            {
1758
                                replaces = -aAlt.Value.Start.CompareTo(bAlt.Value.Start);
×
1759
                            }
1760

1761
                            if (replaces != 0)
1✔
1762
                            {
1763
                                break;
1✔
1764
                            }
1765
                        }
1766
                    }
1767
                }
1768
            }
1769

1770
            return replaces;
1✔
1771
        }
1772

1773
        // Merge new assignments into old so there is only one operation per singleton
1774
        // and we prefer newer assignments.
1775
        private void MergeAssignments(EntityAssignments newAssignments, EntityAssignments old, EntityAssignmentComparer comparer)
1776
        {
1777
            var list = old.Assignments;
1✔
1778
            foreach (var assign in newAssignments.Assignments)
1✔
1779
            {
1780
                // Only one outstanding operation per singleton property
1781
                var add = true;
1✔
1782
                var newList = new List<EntityAssignment>();
1✔
1783
                foreach (var oldAssign in list)
1✔
1784
                {
1785
                    var keep = true;
1✔
1786
                    if (add)
1✔
1787
                    {
1788
                        switch (Replaces(assign, oldAssign))
1✔
1789
                        {
1790
                            case -1: keep = false; break;
1✔
1791
                            case +1: add = false; break;
×
1792
                        }
1793
                    }
1794

1795
                    if (keep)
1✔
1796
                    {
1797
                        newList.Add(oldAssign);
1✔
1798
                    }
1799
                }
1800

1801
                if (add)
1✔
1802
                {
1803
                    newList.Add(assign);
1✔
1804
                }
1805

1806
                list = newList;
1✔
1807
            }
1808

1809
            old.Assignments = list;
1✔
1810
            list.Sort(comparer);
1✔
1811
        }
1✔
1812
    }
1813
}
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

© 2026 Coveralls, Inc