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
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -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
Expand Up @@ -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)
Expand All @@ -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