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

microsoft / botbuilder-dotnet / 389766

28 May 2024 03:50PM UTC coverage: 78.174% (+0.006%) from 78.168%
389766

push

CI-PR build

web-flow
fix: [#6792] Composer Bot with QnA Intent recognized triggers duplicate QnA queries (#6793)

* Fix double QnA trace

* Fix unit tests and create new one

26196 of 33510 relevant lines covered (78.17%)

0.78 hits per line

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

88.72
/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
                            var recognized = actionContext.State.GetValue<RecognizerResult>(TurnPath.Recognized);
1✔
566
                            var activityProcessed = actionContext.State.GetValue<bool>(TurnPath.ActivityProcessed);
1✔
567

568
                            // Avoid reprocessing recognized activity for OnQnAMatch.
569
                            var isOnQnAMatchProcessed = activityProcessed && recognized?.Intents.TryGetValue(OnQnAMatch.QnAMatchIntent, out _) == true;
×
570
                            if (!isOnQnAMatchProcessed)
1✔
571
                            {
572
                                // Recognize utterance (ignore handled)
573
                                var recognizeUtteranceEvent = new DialogEvent
1✔
574
                                {
1✔
575
                                    Name = AdaptiveEvents.RecognizeUtterance,
1✔
576
                                    Value = activity,
1✔
577
                                    Bubble = false
1✔
578
                                };
1✔
579
                                await ProcessEventAsync(actionContext, dialogEvent: recognizeUtteranceEvent, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
580

581
                                // Emit leading RecognizedIntent event
582
                                recognized = actionContext.State.GetValue<RecognizerResult>(TurnPath.Recognized);
1✔
583
                            }
584

585
                            var recognizedIntentEvent = new DialogEvent
1✔
586
                            {
1✔
587
                                Name = AdaptiveEvents.RecognizedIntent,
1✔
588
                                Value = recognized,
1✔
589
                                Bubble = false
1✔
590
                            };
1✔
591
                            handled = await ProcessEventAsync(actionContext, dialogEvent: recognizedIntentEvent, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
592
                        }
593

594
                        // Has an interruption occured?
595
                        // - Setting this value to true causes any running inputs to re-prompt when they're
596
                        //   continued.  The developer can clear this flag if they want the input to instead
597
                        //   process the users uterrance when its continued.
598
                        if (handled)
1✔
599
                        {
600
                            actionContext.State.SetValue(TurnPath.Interrupted, true);
1✔
601
                        }
602

603
                        break;
1✔
604

605
                    case AdaptiveEvents.RecognizeUtterance:
606
                        {
607
                            if (activity.Type == ActivityTypes.Message)
1✔
608
                            {
609
                                // Recognize utterance
610
                                var recognizedResult = await OnRecognizeAsync(actionContext, activity, cancellationToken).ConfigureAwait(false);
1✔
611

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

615
                                // Bug #3572 set these here, because if allowedInterruption is true then event is not emitted, but folks still want the value.
616
                                var (name, score) = recognizedResult.GetTopScoringIntent();
1✔
617
                                actionContext.State.SetValue(TurnPath.TopIntent, name);
1✔
618
                                actionContext.State.SetValue(TurnPath.TopScore, score);
1✔
619
                                actionContext.State.SetValue(DialogPath.LastIntent, name);
1✔
620

621
                                if (Recognizer != null)
1✔
622
                                {
623
                                    await actionContext.DebuggerStepAsync(Recognizer, AdaptiveEvents.RecognizeUtterance, cancellationToken).ConfigureAwait(false);
1✔
624
                                }
625

626
                                handled = true;
1✔
627
                            }
628
                        }
629

630
                        break;
1✔
631

632
                    case AdaptiveEvents.RepromptDialog:
633
                        {
634
                            // AdaptiveDialogs handle new RepromptDialog as it gives access to the dialogContext.
635
                            await this.RepromptDialogAsync(actionContext, actionContext.ActiveDialog, cancellationToken).ConfigureAwait(false);
1✔
636
                            handled = true;
1✔
637
                        }
638

639
                        break;
1✔
640
                }
641
            }
642
            else
643
            {
644
                switch (dialogEvent.Name)
1✔
645
                {
646
                    case AdaptiveEvents.BeginDialog:
647
                        if (actionContext.State.GetBoolValue(TurnPath.ActivityProcessed) == false)
1✔
648
                        {
649
                            var activityReceivedEvent = new DialogEvent
1✔
650
                            {
1✔
651
                                Name = AdaptiveEvents.ActivityReceived,
1✔
652
                                Value = activity,
1✔
653
                                Bubble = false
1✔
654
                            };
1✔
655

656
                            handled = await ProcessEventAsync(actionContext, dialogEvent: activityReceivedEvent, preBubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
657
                        }
658

659
                        break;
1✔
660

661
                    case AdaptiveEvents.ActivityReceived:
662
                        if (activity.Type == ActivityTypes.Message)
1✔
663
                        {
664
                            // Empty sequence?
665
                            if (!actionContext.Actions.Any())
1✔
666
                            {
667
                                // Emit trailing unknownIntent event
668
                                var unknownIntentEvent = new DialogEvent
1✔
669
                                {
1✔
670
                                    Name = AdaptiveEvents.UnknownIntent,
1✔
671
                                    Bubble = false
1✔
672
                                };
1✔
673
                                handled = await ProcessEventAsync(actionContext, dialogEvent: unknownIntentEvent, preBubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
674
                            }
675
                            else
676
                            {
677
                                handled = false;
1✔
678
                            }
679
                        }
680

681
                        // Has an interruption occured?
682
                        // - Setting this value to true causes any running inputs to re-prompt when they're
683
                        //   continued.  The developer can clear this flag if they want the input to instead
684
                        //   process the users uterrance when its continued.
685
                        if (handled)
1✔
686
                        {
687
                            actionContext.State.SetValue(TurnPath.Interrupted, true);
1✔
688
                        }
689

690
                        break;
691
                }
692
            }
693

694
            return handled;
1✔
695
        }
1✔
696

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

711
            // Apply any queued up changes
712
            var actionContext = ToActionContext(dc);
1✔
713
            await actionContext.ApplyChangesAsync(cancellationToken).ConfigureAwait(false);
1✔
714

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

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

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

746
                    // Continue step execution
747
                    result = await actionDC.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
1✔
748
                }
749

750
                // Is the step waiting for input or were we cancelled?
751
                if (result.Status == DialogTurnStatus.Waiting || GetUniqueInstanceId(actionContext) != instanceId)
1✔
752
                {
753
                    return result;
1✔
754
                }
755

756
                // End current step
757
                await EndCurrentActionAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
758

759
                if (result.Status == DialogTurnStatus.CompleteAndWait)
1✔
760
                {
761
                    // Child dialog completed, but wants us to wait for a new activity
762
                    result.Status = DialogTurnStatus.Waiting;
1✔
763
                    return result;
1✔
764
                }
765

766
                var parentChanges = false;
1✔
767
                DialogContext root = actionContext;
1✔
768
                var parent = actionContext.Parent;
1✔
769
                while (parent != null)
1✔
770
                {
771
                    var ac = parent as ActionContext;
1✔
772
                    if (ac != null && ac.Changes != null && ac.Changes.Count > 0)
×
773
                    {
774
                        parentChanges = true;
×
775
                    }
776

777
                    root = parent;
1✔
778
                    parent = root.Parent;
1✔
779
                }
780

781
                // Execute next step
782
                if (parentChanges)
1✔
783
                {
784
                    // Recursively call ContinueDialogAsync() to apply parent changes and continue
785
                    // execution.
786
                    return await root.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
×
787
                }
788

789
                // Apply any local changes and fetch next action
790
                await actionContext.ApplyChangesAsync(cancellationToken).ConfigureAwait(false);
1✔
791
                actionDC = CreateChildContext(actionContext);
1✔
792
                interrupted = true;
1✔
793
            }
1✔
794

795
            return await OnEndOfActionsAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
796
        }
1✔
797

798
        /// <summary>
799
        /// OnSetScopedServices provides ability to set scoped services for the current dialogContext.
800
        /// </summary>
801
        /// <remarks>
802
        /// USe dialogContext.Services.Set(object) to set a scoped object that will be inherited by all children dialogContexts.
803
        /// </remarks>
804
        /// <param name="dialogContext">dialog Context.</param>
805
        protected virtual void OnSetScopedServices(DialogContext dialogContext)
806
        {
807
            if (Generator != null)
1✔
808
            {
809
                dialogContext.Services.Set(this.Generator);
1✔
810
            }
811
        }
1✔
812

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

828
            return Task.FromResult(false);
1✔
829
        }
830

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

845
                if (handled)
1✔
846
                {
847
                    // Still processing assignments
848
                    return await ContinueActionsAsync(actionContext, null, cancellationToken).ConfigureAwait(false);
1✔
849
                }
850
                else if (this.AutoEndDialog.GetValue(actionContext.State))
1✔
851
                {
852
                    actionContext.State.TryGetValue<object>(DefaultResultProperty, out var result);
1✔
853
                    return await actionContext.EndDialogAsync(result, cancellationToken).ConfigureAwait(false);
1✔
854
                }
855

856
                return EndOfTurn;
1✔
857
            }
858

859
            return new DialogTurnResult(DialogTurnStatus.Cancelled);
×
860
        }
1✔
861

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

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

884
                if (result.Intents.Any())
1✔
885
                {
886
                    // Score
887
                    // Gathers all the intents with the highest Score value.
888
                    var scoreSorted = result.Intents.OrderByDescending(e => e.Value.Score).ToList();
1✔
889
                    var topIntents = scoreSorted.TakeWhile(e => e.Value.Score == scoreSorted[0].Value.Score).ToList();
1✔
890

891
                    // Priority
892
                    // Gathers the Intent with the highest Priority (0 being the highest).
893
                    // Note: this functionality is based on the FirstSelector.SelectAsync method.
894
                    var topIntent = topIntents.FirstOrDefault();
1✔
895

896
                    if (topIntents.Count > 1)
1✔
897
                    {
898
                        var highestPriority = double.MaxValue;
1✔
899
                        foreach (var intent in topIntents)
1✔
900
                        {
901
                            var triggerIntent = Triggers.SingleOrDefault(x => x is OnIntent && (x as OnIntent).Intent == intent.Key);
1✔
902
                            var priority = triggerIntent.CurrentPriority(actionContext);
1✔
903
                            if (priority >= 0 && priority < highestPriority)
1✔
904
                            {
905
                                topIntent = intent;
1✔
906
                                highestPriority = priority;
1✔
907
                            }
908
                        }
909
                    }
910

911
                    result.Intents.Clear();
1✔
912
                    result.Intents.Add(topIntent);
1✔
913
                }
914
                else
915
                {
916
                    result.Intents.Add(NoneIntentKey, new IntentScore { Score = 0.0 });
×
917
                }
918

919
                return result;
1✔
920
            }
921

922
            // none intent if there is no recognizer
923
            return new RecognizerResult
1✔
924
            {
1✔
925
                Text = activity.Text ?? string.Empty,
1✔
926
                Intents = new Dictionary<string, IntentScore> { { NoneIntentKey, new IntentScore { Score = 0.0 } } },
1✔
927
            };
1✔
928
        }
1✔
929

930
        /// <summary>
931
        /// Ensures all dependencies for the class are installed.
932
        /// </summary>
933
        protected virtual void EnsureDependenciesInstalled()
934
        {
935
            if (!installedDependencies)
1✔
936
            {
937
                lock (this.syncLock)
1✔
938
                {
939
                    if (!installedDependencies)
1✔
940
                    {
941
                        installedDependencies = true;
1✔
942

943
                        var id = 0;
1✔
944
                        foreach (var trigger in Triggers)
1✔
945
                        {
946
                            if (trigger is IDialogDependencies depends)
1✔
947
                            {
948
                                foreach (var dlg in depends.GetDependencies())
1✔
949
                                {
950
                                    Dialogs.Add(dlg);
1✔
951
                                }
952
                            }
953

954
                            if (trigger.RunOnce)
1✔
955
                            {
956
                                needsTracker = true;
1✔
957
                            }
958

959
                            if (trigger.Priority == null)
1✔
960
                            {
961
                                // Constant expression defined from order
962
                                trigger.Priority = id;
×
963
                            }
964

965
                            if (trigger.Id == null)
1✔
966
                            {
967
                                trigger.Id = id++.ToString(CultureInfo.InvariantCulture);
1✔
968
                            }
969
                        }
970

971
                        // Wire up selector
972
                        if (Selector == null)
1✔
973
                        {
974
                            // Default to most specific then first
975
                            Selector = new MostSpecificSelector { Selector = new FirstSelector() };
1✔
976
                        }
977

978
                        this.Selector.Initialize(Triggers, evaluate: true);
1✔
979
                    }
980
                }
1✔
981
            }
982
        }
1✔
983

984
        // This function goes through the entity assignments and emits events if present.
985
        private async Task<bool> ProcessQueuesAsync(ActionContext actionContext, CancellationToken cancellationToken)
986
        {
987
            DialogEvent evt;
988
            bool handled;
989
            var assignments = EntityAssignments.Read(actionContext);
1✔
990
            var nextAssignment = assignments.NextAssignment();
1✔
991
            if (nextAssignment != null)
1✔
992
            {
993
                object val = nextAssignment;
1✔
994
                if (nextAssignment.Alternative != null)
1✔
995
                {
996
                    val = nextAssignment.Alternatives.ToList();
1✔
997
                }
998

999
                if (nextAssignment.RaisedCount++ == 0)
1✔
1000
                {
1001
                    // Reset retries when new form event is first issued
1002
                    actionContext.State.RemoveValue(DialogPath.Retries);
1✔
1003
                }
1004

1005
                evt = new DialogEvent() { Name = nextAssignment.Event, Value = val, Bubble = false };
1✔
1006
                if (nextAssignment.Event == AdaptiveEvents.AssignEntity)
1✔
1007
                {
1008
                    // 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
1009
                    // @ which is supposed to unwrap down to non-array and @@ which returns the whole thing. @ in the curent code works by doing [0] which
1010
                    // is not enough.
1011
                    var entity = nextAssignment.Value.Value;
1✔
1012
                    if (!(entity is JArray))
1✔
1013
                    {
1014
                        entity = new object[] { entity };
1✔
1015
                    }
1016

1017
                    actionContext.State.SetValue($"{TurnPath.Recognized}.entities.{nextAssignment.Value.Name}", entity);
1✔
1018
                    assignments.Dequeue(actionContext);
1✔
1019
                }
1020

1021
                actionContext.State.SetValue(DialogPath.LastEvent, evt.Name);
1✔
1022
                handled = await this.ProcessEventAsync(actionContext, dialogEvent: evt, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
1023
                if (!handled)
1✔
1024
                {
1025
                    // If event wasn't handled, remove it
1026
                    if (nextAssignment != null && nextAssignment.Event != AdaptiveEvents.AssignEntity)
×
1027
                    {
1028
                        assignments.Dequeue(actionContext);
×
1029
                    }
1030

1031
                    // See if more assignements or end of actions
1032
                    handled = await this.ProcessQueuesAsync(actionContext, cancellationToken).ConfigureAwait(false);
×
1033
                }
1034
            }
1035
            else
1036
            {
1037
                // Emit end of actions
1038
                evt = new DialogEvent() { Name = AdaptiveEvents.EndOfActions, Bubble = false };
1✔
1039
                actionContext.State.SetValue(DialogPath.LastEvent, evt.Name);
1✔
1040
                handled = await this.ProcessEventAsync(actionContext, dialogEvent: evt, preBubble: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
1041
            }
1042

1043
            return handled;
1✔
1044
        }
1✔
1045

1046
        private string GetUniqueInstanceId(DialogContext dc)
1047
        {
1048
            return dc.Stack.Count > 0 ? $"{dc.Stack.Count}:{dc.ActiveDialog.Id}" : string.Empty;
1✔
1049
        }
1050

1051
        private async Task<bool> QueueFirstMatchAsync(ActionContext actionContext, DialogEvent dialogEvent, CancellationToken cancellationToken)
1052
        {
1053
            var selection = await Selector.SelectAsync(actionContext, cancellationToken).ConfigureAwait(false);
1✔
1054
            if (selection.Any())
1✔
1055
            {
1056
                var condition = selection[0];
1✔
1057
                await actionContext.DebuggerStepAsync(condition, dialogEvent, cancellationToken).ConfigureAwait(false);
1✔
1058
                System.Diagnostics.Trace.TraceInformation($"Executing Dialog: {Id} Rule[{condition.Id}]: {condition.GetType().Name}: {condition.GetExpression()}");
1✔
1059

1060
                var properties = new Dictionary<string, string>()
1✔
1061
                {
1✔
1062
                    { "DialogId", Id },
1✔
1063
                    { "Expression", condition.GetExpression().ToString() },
1✔
1064
                    { "Kind", $"Microsoft.{condition.GetType().Name}" },
1✔
1065
                    { "ConditionId", condition.Id },
1✔
1066
                    { "context", TelemetryLoggerConstants.TriggerEvent }
1✔
1067
                };
1✔
1068
                TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
1069

1070
                var changes = await condition.ExecuteAsync(actionContext).ConfigureAwait(false);
1✔
1071

1072
                if (changes != null && changes.Any())
1✔
1073
                {
1074
                    actionContext.QueueChanges(changes[0]);
1✔
1075
                    return true;
1✔
1076
                }
1077
            }
×
1078

1079
            return false;
1✔
1080
        }
1✔
1081

1082
        private ActionContext ToActionContext(DialogContext dc)
1083
        {
1084
            var activeDialogState = dc.ActiveDialog.State as Dictionary<string, object>;
1✔
1085
            var state = activeDialogState[AdaptiveKey] as AdaptiveDialogState;
1✔
1086

1087
            if (state == null)
1✔
1088
            {
1089
                state = new AdaptiveDialogState();
×
1090
                activeDialogState[AdaptiveKey] = state;
×
1091
            }
1092

1093
            if (state.Actions == null)
1✔
1094
            {
1095
                state.Actions = new List<ActionState>();
×
1096
            }
1097

1098
            var actionContext = new ActionContext(dc.Dialogs, dc, new DialogState { DialogStack = dc.Stack }, state.Actions, changeTurnKey);
1✔
1099
            actionContext.Parent = dc.Parent;
1✔
1100
            return actionContext;
1✔
1101
        }
1102

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

1117
                var assignments = EntityAssignments.Read(actionContext);
1✔
1118
                var entities = NormalizeEntities(actionContext);
1✔
1119
                var utterance = activity?.AsMessageActivity()?.Text;
×
1120

1121
                // Utterance is a special entity that corresponds to the full utterance
1122
                entities[UtteranceKey] = new List<EntityInfo>
1✔
1123
                {
1✔
1124
                    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✔
1125
                };
1✔
1126
                var recognized = AssignEntities(actionContext, entities, assignments, lastEvent);
1✔
1127
                var unrecognized = SplitUtterance(utterance, recognized);
1✔
1128

1129
                actionContext.State.SetValue(TurnPath.UnrecognizedText, unrecognized);
1✔
1130
                actionContext.State.SetValue(TurnPath.RecognizedEntities, recognized);
1✔
1131
                assignments.Write(actionContext);
1✔
1132
            }
1133
        }
1✔
1134

1135
        // Split an utterance into unrecognized parts of text
1136
        private List<string> SplitUtterance(string utterance, List<EntityInfo> recognized)
1137
        {
1138
            var unrecognized = new List<string>();
1✔
1139
            var current = 0;
1✔
1140
            foreach (var entity in recognized)
1✔
1141
            {
1142
                if (entity.Start > current)
1✔
1143
                {
1144
                    unrecognized.Add(utterance.Substring(current, entity.Start - current).Trim());
1✔
1145
                }
1146

1147
                current = entity.End;
1✔
1148
            }
1149

1150
            if (current < utterance.Length)
1✔
1151
            {
1152
                unrecognized.Add(utterance.Substring(current));
1✔
1153
            }
1154

1155
            return unrecognized;
1✔
1156
        }
1157

1158
        // Expand object that contains entities which can be op, property or leaf entity
1159
        private void ExpandEntityObject(
1160
            JObject entities, string op, string property, JObject rootInstance, List<string> operations, List<string> properties, uint turn, string text, Dictionary<string, List<EntityInfo>> entityToInfo)
1161
        {
1162
            foreach (var token in entities)
1✔
1163
            {
1164
                var entityName = token.Key;
1✔
1165
                var instances = entities[InstanceKey][entityName] as JArray;
1✔
1166
                ExpandEntities(entityName, token.Value as JArray, instances, rootInstance, op, property, operations, properties, turn, text, entityToInfo);
1✔
1167
            }
1168
        }
1✔
1169

1170
        private string StripProperty(string name)
1171
            => name.EndsWith(PropertyEnding, StringComparison.InvariantCulture) ? name.Substring(0, name.Length - PropertyEnding.Length) : name;
1✔
1172

1173
        // Expand the array of entities for a particular entity
1174
        private void ExpandEntities(
1175
            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)
1176
        {
1177
            if (!name.StartsWith("$", StringComparison.InvariantCulture))
1✔
1178
            {
1179
                // Entities representing schema properties end in "Property" to prevent name collisions with the property itself.
1180
                var propName = StripProperty(name);
1✔
1181
                string entityName = null;
1✔
1182
                var isOp = false;
1✔
1183
                var isProperty = false;
1✔
1184
                if (operations.Contains(name))
1✔
1185
                {
1186
                    op = name;
1✔
1187
                    isOp = true;
1✔
1188
                }
1189
                else if (properties.Contains(propName))
1✔
1190
                {
1191
                    property = propName;
1✔
1192
                    isProperty = true;
1✔
1193
                }
1194
                else
1195
                {
1196
                    entityName = name;
1✔
1197
                }
1198

1199
                for (var entityIndex = 0; entityIndex < entities.Count; ++entityIndex)
1✔
1200
                {
1201
                    var entity = entities[entityIndex];
1✔
1202
                    var instance = instances[entityIndex] as JObject;
1✔
1203
                    var root = rootInstance;
1✔
1204
                    if (root == null)
1✔
1205
                    {
1206
                        // Keep the root entity name and position to help with overlap
1207
                        root = instance.DeepClone() as JObject;
1✔
1208
                        root["type"] = $"{name}{entityIndex}";
1✔
1209
                    }
1210

1211
                    if (entityName != null)
1✔
1212
                    {
1213
                        ExpandEntity(entityName, entity, instance, root, op, property, turn, text, entityToInfo);
1✔
1214
                    }
1215
                    else if (entity is JObject entityObject)
1✔
1216
                    {
1217
                        if (entityObject.Count == 0)
1✔
1218
                        {
1219
                            if (isOp)
1✔
1220
                            {
1221
                                // Handle operator with no children
1222
                                ExpandEntity(op, null, instance, root, op, property, turn, text, entityToInfo);
×
1223
                            }
1224
                            else if (isProperty)
1✔
1225
                            {
1226
                                // Handle property with no children
1227
                                ExpandEntity(property, null, instance, root, op, property, turn, text, entityToInfo);
1✔
1228
                            }
1229
                        }
1230
                        else
1231
                        {
1232
                            ExpandEntityObject(entityObject, op, property, root, operations, properties, turn, text, entityToInfo);
1✔
1233
                        }
1234
                    }
1235
                    else if (isOp)
×
1236
                    {
1237
                        // Handle global operator with no children in model
1238
                        ExpandEntity(op, null, instance, root, op, property, turn, text, entityToInfo);
×
1239
                    }
1240
                }
1241
            }
1242
        }
1✔
1243

1244
        // Expand a leaf entity into EntityInfo.
1245
        private void ExpandEntity(string name, object value, dynamic instance, dynamic rootInstance, string op, string property, uint turn, string text, Dictionary<string, List<EntityInfo>> entityToInfo)
1246
        {
1247
            if (instance != null && rootInstance != null)
×
1248
            {
1249
                if (!entityToInfo.TryGetValue(name, out List<EntityInfo> infos))
1✔
1250
                {
1251
                    infos = new List<EntityInfo>();
1✔
1252
                    entityToInfo[name] = infos;
1✔
1253
                }
1254

1255
                var info = new EntityInfo
×
1256
                {
×
1257
                    WhenRecognized = turn,
×
1258
                    Name = name,
×
1259
                    Value = value,
×
1260
                    Operation = op,
×
1261
                    Property = property,
×
1262
                    Start = (int)rootInstance.startIndex,
×
1263
                    End = (int)rootInstance.endIndex,
×
1264
                    RootEntity = rootInstance.type,
×
1265
                    Text = (string)(rootInstance.text ?? string.Empty),
×
1266
                    Type = (string)(instance.type ?? null),
×
1267
                    Score = (double)(instance.score ?? 0.0d),
×
1268
                    Priority = 0,
×
1269
                };
×
1270

1271
                info.Coverage = (info.End - info.Start) / (double)text.Length;
1✔
1272
                infos.Add(info);
1✔
1273
            }
1274
        }
1✔
1275

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

1323
            // When there are multiple possible resolutions for the same entity that overlap, pick the one that covers the
1324
            // most of the utterance.
1325
            foreach (var infos in entityToInfo.Values)
1✔
1326
            {
1327
                infos.Sort((entity1, entity2) =>
1✔
1328
                {
1✔
1329
                    var val = 0;
1✔
1330
                    if (entity1.Start == entity2.Start)
1✔
1331
                    {
1✔
1332
                        if (entity1.End > entity2.End)
1✔
1333
                        {
1✔
1334
                            val = -1;
×
1335
                        }
1✔
1336
                        else if (entity1.End < entity2.End)
1✔
1337
                        {
1✔
1338
                            val = +1;
×
1339
                        }
1✔
1340
                    }
1✔
1341
                    else if (entity1.Start < entity2.Start)
1✔
1342
                    {
1✔
1343
                        val = -1;
1✔
1344
                    }
1✔
1345
                    else
1✔
1346
                    {
1✔
1347
                        val = +1;
×
1348
                    }
1✔
1349

1✔
1350
                    return val;
1✔
1351
                });
1✔
1352
                for (var i = 0; i < infos.Count; ++i)
1✔
1353
                {
1354
                    var current = infos[i];
1✔
1355
                    for (var j = i + 1; j < infos.Count;)
1✔
1356
                    {
1357
                        var alt = infos[j];
1✔
1358
                        if (current.Covers(alt))
1✔
1359
                        {
1360
                            _ = infos.Remove(alt);
1✔
1361
                        }
1362
                        else
1363
                        {
1364
                            ++j;
1✔
1365
                        }
1366
                    }
1367
                }
1368
            }
1369

1370
            return entityToInfo;
1✔
1371
        }
1372

1373
        // An entity matches an assignment if the detected operation/property match
1374
        private bool MatchesAssignment(EntityInfo entity, EntityAssignment assignment)
1375
         => (entity.Operation == null || entity.Operation == assignment.Operation)
×
1376
            && (entity.Property == null || entity.Property == assignment.Property);
×
1377

1378
        // Generate candidate assignments including property and operation
1379
        private IEnumerable<EntityAssignment> Candidates(Dictionary<string, List<EntityInfo>> entities, string[] expected, string lastEvent, EntityAssignment nextAssignment, JObject askDefault, JObject dialogDefault)
1380
        {
1381
            var globalExpectedOnly = dialogSchema.Schema[ExpectedOnlyKey]?.ToObject<List<string>>() ?? new List<string>();
1✔
1382
            var requiresValue = dialogSchema.Schema[RequiresValueKey]?.ToObject<List<string>>() ?? new List<string>();
1✔
1383
            var assignments = new List<EntityAssignment>();
1✔
1384

1385
            // Add entities with a recognized property
1386
            foreach (var alternatives in entities.Values)
1✔
1387
            {
1388
                foreach (var alternative in alternatives)
1✔
1389
                {
1390
                    if (alternative.Property != null && (alternative.Value != null || !requiresValue.Contains(alternative.Operation)))
1✔
1391
                    {
1392
                        assignments.Add(new EntityAssignment
1✔
1393
                        {
1✔
1394
                            Value = alternative,
1✔
1395
                            Property = alternative.Property,
1✔
1396
                            Operation = alternative.Operation,
1✔
1397
                            IsExpected = expected.Contains(alternative.Property)
1✔
1398
                        });
1✔
1399
                    }
1400
                }
1401
            }
1402

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

1441
            // Add default operations
1442
            foreach (var assignment in assignments)
1✔
1443
            {
1444
                if (assignment.Operation == null)
1✔
1445
                {
1446
                    // Assign missing operation
1447
                    if (lastEvent == AdaptiveEvents.ChooseEntity
1✔
1448
                        && assignment.Value.Property == nextAssignment.Property)
1✔
1449
                    {
1450
                        // Property and value match ambiguous entity
1451
                        assignment.Operation = AdaptiveEvents.ChooseEntity;
1✔
1452
                        assignment.IsExpected = true;
1✔
1453
                    }
1454
                    else
1455
                    {
1456
                        // Assign default operator
1457
                        assignment.Operation = DefaultOperation(assignment, askDefault, dialogDefault);
1✔
1458
                    }
1459
                }
1460
            }
1461

1462
            // Add choose property matches
1463
            if (lastEvent == AdaptiveEvents.ChooseProperty)
1✔
1464
            {
1465
                foreach (var alternatives in entities.Values)
1✔
1466
                {
1467
                    foreach (var alternative in alternatives)
1✔
1468
                    {
1469
                        if (alternative.Value == null)
1✔
1470
                        {
1471
                            // If alternative matches one alternative it answers chooseProperty
1472
                            var matches = nextAssignment.Alternatives.Where(a => MatchesAssignment(alternative, a));
1✔
1473
                            if (matches.Count() == 1)
1✔
1474
                            {
1475
                                assignments.Add(new EntityAssignment
1✔
1476
                                {
1✔
1477
                                    Value = alternative,
1✔
1478
                                    Operation = AdaptiveEvents.ChooseProperty,
1✔
1479
                                    IsExpected = true
1✔
1480
                                });
1✔
1481
                            }
1482
                        }
1483
                    }
1484
                }
1485
            }
1486

1487
            // Add pure operations
1488
            foreach (var alternatives in entities.Values)
1✔
1489
            {
1490
                foreach (var alternative in alternatives)
1✔
1491
                {
1492
                    if (alternative.Operation != null && alternative.Property == null && alternative.Value == null)
×
1493
                    {
1494
                        var assignment = new EntityAssignment
×
1495
                        {
×
1496
                            Value = alternative,
×
1497
                            Property = null,
×
1498
                            Operation = alternative.Operation,
×
1499
                            IsExpected = false
×
1500
                        };
×
1501
                        assignments.Add(assignment);
×
1502
                    }
1503
                }
1504
            }
1505

1506
            // Preserve expectedProperties if there is no property
1507
            foreach (var assignment in assignments)
1✔
1508
            {
1509
                if (assignment.Property == null)
1✔
1510
                {
1511
                    assignment.ExpectedProperties = expected.ToList();
1✔
1512
                }
1513
            }
1514

1515
            return assignments;
1✔
1516
        }
1517

1518
        private void AddAssignment(EntityAssignment assignment, EntityAssignments assignments)
1519
        {
1520
            // Entities without a property or operation are available as entities only when found
1521
            if (assignment.Property != null || assignment.Operation != null)
×
1522
            {
1523
                if (assignment.Alternative != null)
1✔
1524
                {
1525
                    assignment.Event = AdaptiveEvents.ChooseProperty;
1✔
1526
                }
1527
                else if (assignment.Value.Value is JArray arr)
1✔
1528
                {
1529
                    if (arr.Count > 1)
1✔
1530
                    {
1531
                        assignment.Event = AdaptiveEvents.ChooseEntity;
1✔
1532
                    }
1533
                    else
1534
                    {
1535
                        assignment.Event = AdaptiveEvents.AssignEntity;
1✔
1536
                        assignment.Value.Value = arr[0];
1✔
1537
                    }
1538
                }
1539
                else
1540
                {
1541
                    assignment.Event = AdaptiveEvents.AssignEntity;
1✔
1542
                }
1543

1544
                assignments.Assignments.Add(assignment);
1✔
1545
            }
1546
        }
1✔
1547

1548
        // Have each property pick which overlapping entity is the best one
1549
        // This can happen because LUIS will return both 'wheat' and 'whole wheat' as the same list entity.
1550
        private IEnumerable<EntityAssignment> RemoveOverlappingPerProperty(IEnumerable<EntityAssignment> candidates)
1551
        {
1552
            var perProperty = from candidate in candidates
1✔
1553
                              group candidate by candidate.Property;
1✔
1554
            foreach (var propChoices in perProperty)
1✔
1555
            {
1556
                var entityPreferences = dialogSchema.PathToSchema(propChoices.Key).Entities;
1✔
1557
                var choices = propChoices.ToList();
1✔
1558

1559
                // Assume preference by order listed in mappings
1560
                // Alternatives would be to look at coverage or other metrics
1561
                foreach (var entity in entityPreferences)
1✔
1562
                {
1563
                    EntityAssignment candidate;
1564
                    do
1565
                    {
1566
                        candidate = null;
1✔
1567
                        foreach (var mapping in choices)
1✔
1568
                        {
1569
                            if (mapping.Value.Name == entity)
1✔
1570
                            {
1571
                                candidate = mapping;
1✔
1572
                                break;
1✔
1573
                            }
1574
                        }
1575

1576
                        if (candidate != null)
1✔
1577
                        {
1578
                            // Remove any overlapping entities without a common root
1579
                            choices.RemoveAll(choice => choice == candidate || (!choice.Value.SharesRoot(candidate.Value) && choice.Value.Overlaps(candidate.Value)));
1✔
1580
                            yield return candidate;
1✔
1581
                        }
1582
                    }
1583
                    while (candidate != null);
1✔
1584
                }
1✔
1585

1586
                // Keep remaining properties for things like show/clear that are not property specific
1587
                foreach (var choice in choices)
1✔
1588
                {
1589
                    yield return choice;
1✔
1590
                }
1591
            }
1✔
1592
        }
1✔
1593

1594
        // Return the default operation for an assignment by looking at the per-ask and dialog defaults
1595
        private string DefaultOperation(EntityAssignment assignment, JObject askDefault, JObject dialogDefault)
1596
        {
1597
            string operation = null;
1✔
1598
            if (assignment.Property != null)
1✔
1599
            {
1600
                if (askDefault != null && (askDefault.TryGetValue(assignment.Value.Name, out var askOp) || askDefault.TryGetValue(string.Empty, out askOp)))
×
1601
                {
1602
                    operation = askOp.Value<string>();
×
1603
                }
1604
                else if (dialogDefault != null
1✔
1605
                        && (dialogDefault.TryGetValue(assignment.Property, out var entities)
1✔
1606
                            || dialogDefault.TryGetValue(string.Empty, out entities))
1✔
1607
                        && ((entities as JObject).TryGetValue(assignment.Value.Name, out var dialogOp)
1✔
1608
                            || (entities as JObject).TryGetValue(string.Empty, out dialogOp)))
1✔
1609
                {
1610
                    operation = dialogOp.Value<string>();
1✔
1611
                }
1612
            }
1613

1614
            return operation;
1✔
1615
        }
1616

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

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

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

1638
            var nextAssignment = existing.NextAssignment();
1✔
1639
            var candidates = (from candidate in RemoveOverlappingPerProperty(Candidates(entities, expected, lastEvent, nextAssignment, askDefaultOp, defaultOp))
1✔
1640
                              orderby
1✔
1641
                                candidate.IsExpected descending,
1✔
1642
                                candidate.Operation == DefaultOperation(candidate, askDefaultOp, defaultOp) descending
1✔
1643
                              select candidate).ToList();
1✔
1644
            var usedEntities = new HashSet<EntityInfo>(from candidate in candidates select candidate.Value);
1✔
1645
            List<string> expectedChoices = null;
1✔
1646
            var choices = new List<EntityAssignment>();
1✔
1647
            while (candidates.Any())
1✔
1648
            {
1649
                var candidate = candidates.First();
1✔
1650

1651
                // Alternatives are either for the same entity or from different roots
1652
                var alternatives = (from alt in candidates
1✔
1653
                                    where candidate.Value.Overlaps(alt.Value) && (!candidate.Value.SharesRoot(alt.Value) || candidate.Value == alt.Value)
1✔
1654
                                    select alt).ToList();
1✔
1655
                candidates = candidates.Except(alternatives).ToList();
1✔
1656
                foreach (var alternative in alternatives)
1✔
1657
                {
1658
                    usedEntities.Add(alternative.Value);
1✔
1659
                }
1660

1661
                if (candidate.IsExpected && candidate.Value.Name != UtteranceKey)
1✔
1662
                {
1663
                    // If expected binds entity, drop unexpected alternatives unless they have an explicit operation
1664
                    alternatives.RemoveAll(a => !a.IsExpected && a.Value.Operation == null);
1✔
1665
                }
1666

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

1670
                // Remove all alternatives that are fully contained in largest
1671
                alternatives.RemoveAll(a => candidate.Value.Covers(a.Value));
1✔
1672

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

1709
                        AddAssignment(choice, assignments);
1✔
1710
                        choices.RemoveAll(c => c.Value.Overlaps(choice.Value));
1✔
1711
                        mapped = true;
1✔
1712
                    }
1713
                }
1714

1715
                candidate.AddAlternatives(alternatives);
1✔
1716
                if (!mapped)
1✔
1717
                {
1718
                    AddAssignment(candidate, assignments);
1✔
1719
                }
1720
            }
1721

1722
            if (expectedChoices != null)
1✔
1723
            {
1724
                // When choosing between property assignments, make the assignments be expected.
1725
                if (expectedChoices.Any())
1✔
1726
                {
1727
                    actionContext.State.SetValue(DialogPath.ExpectedProperties, expectedChoices);
1✔
1728
                }
1729

1730
                // Add back in any non-overlapping choices that have not been resolved
1731
                while (choices.Any())
1✔
1732
                {
1733
                    var choice = choices.First();
×
1734
                    var overlaps = from alt in choices where choice.Value.Overlaps(alt.Value) select alt;
×
1735
                    choice.AddAlternatives(overlaps);
×
1736
                    AddAssignment(choice, assignments);
×
1737
                    choices.RemoveAll(c => c.Value.Overlaps(choice.Value));
×
1738
                }
1739

1740
                existing.Dequeue(actionContext);
1✔
1741
            }
1742

1743
            var operations = new EntityAssignmentComparer(dialogSchema.Schema[OperationsKey]?.ToObject<string[]>() ?? Array.Empty<string>());
×
1744
            MergeAssignments(assignments, existing, operations);
1✔
1745
            return usedEntities.ToList();
1✔
1746
        }
1747

1748
        // 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
1749
        // -1 a replaces b
1750
        //  0 no replacement
1751
        // +1 b replaces a
1752
        private int Replaces(EntityAssignment a, EntityAssignment b)
1753
        {
1754
            var replaces = 0;
1✔
1755
            foreach (var aAlt in a.Alternatives)
1✔
1756
            {
1757
                foreach (var bAlt in b.Alternatives)
1✔
1758
                {
1759
                    if (aAlt.Property == bAlt.Property && aAlt.Value.Value != null && bAlt.Value.Value != null)
1✔
1760
                    {
1761
                        var prop = dialogSchema.PathToSchema(aAlt.Property);
1✔
1762
                        if (!prop.IsArray)
1✔
1763
                        {
1764
                            replaces = -aAlt.Value.WhenRecognized.CompareTo(bAlt.Value.WhenRecognized);
1✔
1765
                            if (replaces == 0)
1✔
1766
                            {
1767
                                replaces = -aAlt.Value.Start.CompareTo(bAlt.Value.Start);
×
1768
                            }
1769

1770
                            if (replaces != 0)
1✔
1771
                            {
1772
                                break;
1✔
1773
                            }
1774
                        }
1775
                    }
1776
                }
1777
            }
1778

1779
            return replaces;
1✔
1780
        }
1781

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

1804
                    if (keep)
1✔
1805
                    {
1806
                        newList.Add(oldAssign);
1✔
1807
                    }
1808
                }
1809

1810
                if (add)
1✔
1811
                {
1812
                    newList.Add(assign);
1✔
1813
                }
1814

1815
                list = newList;
1✔
1816
            }
1817

1818
            old.Assignments = list;
1✔
1819
            list.Sort(comparer);
1✔
1820
        }
1✔
1821
    }
1822
}
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