Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Users/hossein/silence detection #1336

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -403,4 +403,5 @@ experimental/generator-dotnet-yeoman/node_modules
package-lock.json

# Ignore test bots
testing/*
testing/*
packages/.vs/*
21 changes: 21 additions & 0 deletions packages/Telephony/Actions/ITimeoutInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using AdaptiveExpressions.Properties;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.Bot.Components.Telephony.Actions
{
public interface ITimeoutInput
{
// Summary:
// Defines dialog context state property value.
const string SilenceDetected = "dialog.silenceDetected";

/// <summary>
/// Gets or sets a value indicating how long to wait for before timing out and using the default value.
/// </summary>
[JsonProperty("timeOutInMilliseconds")]
IntExpression TimeOutInMilliseconds { get; set; }
}
}
120 changes: 13 additions & 107 deletions packages/Telephony/Actions/TimeoutChoiceInput.cs
Original file line number Diff line number Diff line change
@@ -16,12 +16,16 @@

namespace Microsoft.Bot.Components.Telephony.Actions
{
public class TimeoutChoiceInput : ChoiceInput
public class TimeoutChoiceInput : ChoiceInput, ITimeoutInput
{
[JsonProperty("$kind")]
public new const string Kind = "Microsoft.Telephony.TimeoutChoiceInput";

private static IStateMatrix stateMatrix = new LatchingStateMatrix();
/// <summary>
/// Gets or sets a value indicating how long to wait for before timing out and using the default value.
/// </summary>
[JsonProperty("timeOutInMilliseconds")]
public IntExpression TimeOutInMilliseconds { get; set; }

[JsonConstructor]
public TimeoutChoiceInput([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
@@ -31,118 +35,20 @@ public TimeoutChoiceInput([CallerFilePath] string sourceFilePath = "", [CallerLi
this.RegisterSourceLocation(sourceFilePath, sourceLineNumber);
}

/// <summary>
/// Gets or sets a value indicating how long to wait for before timing out and using the default value.
/// </summary>
[JsonProperty("timeOutInMilliseconds")]
public IntExpression TimeOutInMilliseconds { get; set; }

public async override Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (options is CancellationToken)
{
throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
}

if (this.Disabled != null && this.Disabled.GetValue(dc.State) == true)
{
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}

//start a timer that will continue this conversation
var timerId = Guid.NewGuid().ToString();
CreateTimerForConversation(dc, timerId, cancellationToken);
await stateMatrix.StartAsync(timerId).ConfigureAwait(false);
dc.State.SetValue("this.TimerId", timerId);

return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false);
return await TimeoutInput.BeginDialogAsync(this, dc,
base.BeginDialogAsync,
options, cancellationToken);
}

public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
var activity = dc.Context.Activity;

//Handle case where we timed out
var interrupted = dc.State.GetValue<bool>(TurnPath.Interrupted, () => false);
if (!interrupted && activity.Type != ActivityTypes.Message && activity.Name == ActivityEventNames.ContinueConversation)
{
//Set max turns so that we evaluate the default when we visit the inputdialog.
MaxTurnCount = 1;

//We need to set interrupted here or it will discard the continueconversation event...
dc.State.SetValue(TurnPath.Interrupted, true);
return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
}
else
{
//If we didn't timeout then we have to manage our timer somehow.
//For starters, complete our existing timer.
var timerId = dc.State.GetValue<string>("this.TimerId");

//Should never happen but if it does, it shouldn't be fatal.
if (timerId != null)
{
await stateMatrix.CompleteAsync(timerId).ConfigureAwait(false);
}

//Begin dirty hack to start a timer for the reprompt

//If our input was not valid, restart the timer.
dc.State.SetValue(VALUE_PROPERTY, activity.Text); //OnRecognizeInput assumes this was already set earlier on in the dialog. We will set it and then unset it to simulate passing an argument to a function... :D
if (await OnRecognizeInputAsync(dc, cancellationToken).ConfigureAwait(false) != InputState.Valid)
{
//We are cheating to force this recognition here. Maybe not good?

//Known bug: Sometimes invalid input gets accepted anyway(due to max turns and defaulting rules), this will start a continuation for a thing it shouldn't.
//Sure do wish EndDialog was available to the adaptive stack.

var newTimerId = Guid.NewGuid().ToString();
CreateTimerForConversation(dc, newTimerId, cancellationToken);
await stateMatrix.StartAsync(newTimerId).ConfigureAwait(false);
}

//Clear our the input property after recognition since it will happen again later :D
dc.State.SetValue(VALUE_PROPERTY, null);

//End dirty hack
}

return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
return await TimeoutInput.ContinueDialogAsync(this, dc, VALUE_PROPERTY, TURN_COUNT_PROPERTY,
OnRecognizeInputAsync,
base.ContinueDialogAsync,
cancellationToken);
}

private void CreateTimerForConversation(DialogContext dc, string timerId, CancellationToken cancellationToken)
{
BotAdapter adapter = dc.Context.Adapter;
var identity = dc.Context.TurnState.Get<ClaimsIdentity>("BotIdentity");

var appId = identity?.Claims?.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value;
ConversationReference conversationReference = dc.Context.Activity.GetConversationReference();
int timeout = TimeOutInMilliseconds.GetValue(dc.State);

//Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
Task.Run(async () =>
{
await Task.Delay(timeout).ConfigureAwait(false);
string msAppId = appId;

// If the channel is the Emulator, and authentication is not in use,
// the AppId will be null. We generate a random AppId for this case only.
// This is not required for production, since the AppId will have a value.
if (string.IsNullOrEmpty(msAppId))
{
msAppId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
}

//if we aren't already complete, go ahead and timeout
await stateMatrix.RunForStatusAsync(timerId, StateStatus.Running, async () =>
{
await adapter.ContinueConversationAsync(
msAppId,
conversationReference,
BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component
cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
});
}
}
}
Loading