Waterfall Dialog not executes all steps when called from other Dialog - c#

I'm building a bot connected to LUIS to get intents and with some dialogs that make other things.
I have the following dialogs:
MainDialog: that calls LUIS to get intents
DialogOne: with some steps
DialogTwo: with some steps
ScoreDialog: with a survey to get opinnion from user after other dialogs. this dialog can be called from others.
MainDialog is the entry point and this is the code:
public class MainDialog : ComponentDialog
{
public MainDialog() : base($"{nameof(MainDialog)}.main")
{
var waterfallSteps = new WaterfallStep[]
{
RedirectStepAsync
};
AddDialog(new WaterfallDialog($"{nameof(MainDialog)}.main", waterfallSteps));
AddDialog(new DialogOne($"{nameof(MainDialog)}.dialogOne"));
AddDialog(new DialogTwo($"{nameof(MainDialog)}.dialogTwo"));
InitialDialogId = $"{nameof(MainDialog)}.main";
}
private async Task<DialogTurnResult> RedirectStepAsync(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await waterfallStepContext.Context.SendActivityAsync("What you want?");
var result = await _myLuisService.RecognizeAsync(waterfallStepContext.Context, cancellationToken);
var topIntent = result.GetTopScoringIntent();
switch (topIntent.intent)
{
case "None": break;
case "IntentOne":
await waterfallStepContext.BeginDialogAsync($"{nameof(MainDialog)}.dialogOne", null, cancellationToken);
break;
case "IntentTwo":
await waterfallStepContext.BeginDialogAsync($"{nameof(MainDialog)}.dialogTwo", null, cancellationToken);
break;
}
return await waterfallStepContext.EndDialogAsync(null, cancellationToken);
}
}
Note that MainDialog only have one step that calls to LUIS in order to get user intent.
When user intent is received I switch to another specific dialog with 'BeginDialogAsync' method
For example, we are switching to DialogOne.
This is the code:
public class DialogOne : ComponentDialog
{
public DialogOne(string dialogId) : base(dialogId)
{
var waterfallSteps = new WaterfallStep[]
{
StepOne,
StepTwo,
StepThree
};
AddDialog(new WaterfallDialog($"{nameof(DialogOne)}.main", waterfallSteps));
AddDialog(new TextPrompt($"{nameof(ScoreDialog)}.score"));
InitialDialogId = $"{nameof(DialogOne)}.main";
}
private async Task<DialogTurnResult> StepOne(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
// do something...
}
private async Task<DialogTurnResult> StepTwo(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
// do something...
}
private async Task<DialogTurnResult> StepThree(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await waterfallStepContext.BeginDialogAsync($"{nameof(DialogOne)}.score", null, cancellationToken);
}
}
This DialogOne contains some steps, and when I finish, I want to show a new one dialog to ask their opinnion about the process. It's a votting process.
Note that I'm using await waterfallStepContext.BeginDialogAsync($"{nameof(DialogOne)}.score", null, cancellationToken); to call ScoreDialog
Finally, the ScoreDialog prompt two options to vote and then, in the next steps we capture and process the response, we thank the user for their vote...
public class ScoreDialog : ComponentDialog
{
public ScoreDialog(string dialogId) : base(dialogId)
{
var waterfallSteps = new WaterfallStep[]
{
AskForScoreStepAsync,
GratitudeForScoreStepAsync
};
AddDialog(new WaterfallDialog($"{nameof(ScoreDialog)}.main", waterfallSteps));
AddDialog(new TextPrompt($"{nameof(ScoreDialog)}.ask"));
InitialDialogId = $"{nameof(ScoreDialog)}.main";
}
private async Task<DialogTurnResult> AskForScoreStepAsync(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await SaveConversationAsync(waterfallStepContext, data);
return await waterfallStepContext.PromptAsync(
$"{nameof(ScoreDialog)}.ask",
new PromptOptions()
{
Prompt = CreateScoreButtons()
},
cancellationToken);
}
private Activity CreateScoreButtons()
{
var reply = MessageFactory.Text(string.Empty);
reply.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
new() { Title = "I like", Value = "I like", Type = ActionTypes.ImBack },
new() { Title = "I dislike", Value = "I like", Type = ActionTypes.ImBack },
}
};
return reply;
}
private async Task<DialogTurnResult> GratitudeForScoreStepAsync(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await waterfallStepContext.Context.SendActivityAsync("Thanks for voting!");
//TODO: do something with the vote
return await waterfallStepContext.EndDialogAsync(null, cancellationToken);
}
}
But, the issue is when I redirects to ScoreDialog. This dialog only shows the first step (AskForScoreStepAsync), but no continues to the last (AskForScoreStepAsync).
What is does is return to MainDialog with the value "I like" or "I dislike" from previous step... this "I like" or "I dislike" text is passed again to LUIS with wrong result (because isn't a valid utterance)
So, what's wrong?
How can I redirect the control from dialog to another dialog, and on this new one dialog, complete all steps?

Related

How to pass in options to a Dialog when using TestFlow - Azure botframework v4 - C# - Unit Tests

Using Azure Bot framework v4 - C# ... xunit...
I have the following unit test for testing a Component Dialog that in turn uses an AdaptiveDialog.
As part of unit testing the dialog, I want to pass in options to the dialog - the options that get passed to the OnBeginDialogAsync overload of the Dialog class.
Any thoughts on how we can pass myDialogOptions as the options to the dialog?
Thank you
Regards
Athadu
public class ConfirmationDialog : ComponentDialog
{
public class Options
{
public string PromptTemplate { get; set; }
}
public ConfirmationDialog()
: base("test")
{
}
protected override Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default)
{
//
// Avoiding using BotState here. Instead ... use options...
// do something based on passed in options
//
}
}
[Fact]
public async Task TestMyComponentDialogThatUsesAdaptiveDialog()
{
//Arrange
Setup();
TestAdapter = (TestAdapter)new TestAdapter("my")
.UseStorage(memoryStorage)
.UseBotState(UserState, ConversationState)
.Use(Middlewares[0]);
var dialogState = ConversationState.CreateProperty<DialogState>("dialogState");
var dialogToTest = new ConfirmationDialog();
var dialogManager = new DialogManager(dialogToTest);
var myDialogOptions = new MyOptions { Name = "Jon Doe" };
await new TestFlow(TestAdapter, async (turnContext, cancellationToken) =>
{
<<<<<< How to pass in Dialog Options myDialogOptions to the dialog - need to access it within OnBeginDialogAsync >>>>>
<<<<<< of Dialog class override method OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default) >>>>>
var result = await dialogManager.OnTurnAsync(turnContext, cancellationToken);
})
//Act
.SendConversationUpdate()
//Assert
.AssertReply(activity =>
{
var resolvedActivity = activity.AsMessageActivity();
resolvedActivity.Text.Should().StartWith("Some Text");
}, null, 2100)
.StartTestAsync();
}
You can see in the source code that dialog managers don't pass any options to their root dialogs:
private async Task<DialogTurnResult> HandleBotOnTurnAsync(DialogContext dc, CancellationToken cancellationToken)
{
DialogTurnResult turnResult;
// the bot is running as a root bot.
if (dc.ActiveDialog == null)
{
// start root dialog
turnResult = await dc.BeginDialogAsync(_rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
// Continue execution
// - This will apply any queued up interruptions and execute the current/next step(s).
turnResult = await dc.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (turnResult.Status == DialogTurnStatus.Empty)
{
// restart root dialog
turnResult = await dc.BeginDialogAsync(_rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
await SendStateSnapshotTraceAsync(dc, "Bot State", cancellationToken).ConfigureAwait(false);
return turnResult;
}
If you want to pass options to BeginDialogAsync then you should call that or PromptAsync yourself.

How do you detect active Dialog in On MessageActivityAsync

I have created a DispatchBot using Luis and QnA and I want to additionally use multiple Dialogs (one for each intent.
Everything is working except if the dialog needs to prompt for a question. If you get the all the entities in the first utterance you can respond and all is good, however if you need to request further info that is when it fails. When the user responds it goes back to OnMessageActivityAsync and then forgets about the dialog.
I understand that I need to run RunAsync(..) to reopen the dialog however I can't get the right context. Everything I have tried either opens the dialog with null Accessors/DialogState or fails to open the dialog.
I am very new to Azure Bot Framework and I have spent days googling but each example doesn't do everything I need to do.
My bot is as follows:
public class DispatchBot : ActivityHandler
{
private readonly ILogger<DispatchBot> _logger;
private readonly IBotServices _botServices;
private readonly DialogSet _dialogSet;
private readonly MilaAccessors _milaAccessors;
private readonly BotState _userState;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger, MilaAccessors accessors)
{
_logger = logger;
_botServices = botServices;
_dialogSet = new DialogSet(accessors.ConversationDialogState);
_milaAccessors = accessors;
_userState = accessors.UserState;
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
await base.OnMessageActivityAsync(turnContext, cancellationToken);
await _milaAccessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
List<Dialog> dialogs = _dialogSet.GetDialogs().ToList();
if (dialogs.Any()) //This is always false
{
//If count is greater than zero, then you can continue dialog conversation.
await dialogs.First().RunAsync(turnContext, _milaAccessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
else
{
// First, we use the dispatch model to determine which cognitive service (LUIS or QnA) to use.
var recognizerResult = await _botServices.Dispatch.RecognizeAsync(turnContext, cancellationToken);
// Top intent tell us which cognitive service to use.
var topIntent = recognizerResult.GetTopScoringIntent();
// Next, we call the dispatcher with the top intent.
if (topIntent.score > 0.5)
{
await DispatchToTopIntentAsync(turnContext, topIntent.intent, recognizerResult, cancellationToken);
}
else
{
await ProcessQnAAsync(turnContext, cancellationToken);
}
}
}
private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case "Form15":
await Form15IntentAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "Layouts":
await ProcessLayoutsAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "Weather":
await ProcessWeatherAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
default:
await ProcessQnAAsync(turnContext, cancellationToken);
break;
}
}
}
And the Dialogs are of the form:
public class Form15Dialog : ComponentDialog
{
private const string UserInfo = "form15-userInfo";
private readonly MilaAccessors _milaAccessors;
private readonly string DlgAddressId = "AddressDlg";
private readonly string Form15Id = "Form15DialogName";
private readonly BotState _userState;
private readonly BotState _conversationState;
public Form15Dialog(MilaAccessors milaAccessors) : base(nameof(Form15Dialog))
{
_milaAccessors = milaAccessors;
_userState = milaAccessors.UserState;
_conversationState = milaAccessors.ConversationState;
AddDialog(new TextPrompt(DlgAddressId, AddressValidation));
AddDialog(new WaterfallDialog(nameof(Form15Id), new WaterfallStep[]
{
InitialiseStepAsync,
GetAddressStepAsync,
DisplayForm15StepAsync
}));
InitialDialogId = nameof(Form15Id);
}
private async Task<DialogTurnResult> InitialiseStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
stepContext.Values["MilaAccessors"] = _milaAccessors;
UserProfile userProfile = _milaAccessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken).Result;
Form15DialogValues userInfo = new Form15DialogValues {Name = userProfile.Name, MilaSessionId = userProfile.SessionId};
IList<EntityModel> options = (IList<EntityModel>)stepContext.Options;
foreach (EntityModel model in options)
{
switch (model.Type)
{
case "JobNumber":
userInfo.JobNumber = model.Entity;
break;
case "Sections":
userInfo.Sections = model.Entity;
break;
case "StreetAddress":
userInfo.StreetAddress = model.Entity;
break;
case "Suburb":
userInfo.Suburb = model.Entity;
break;
case "PostCode":
userInfo.PostCode = model.Entity;
break;
}
}
// Create an object in which to collect the user's information within the dialog.
stepContext.Values[UserInfo] = userInfo;
if (UpdateUserInfoFromWebService(userInfo))
{
stepContext.Values[UserInfo] = userInfo;
return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
await _userState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
await _conversationState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
PromptOptions promptOptions = new PromptOptions { Prompt = MessageFactory.Text("Could you give me the full Job Address?") };
return await stepContext.PromptAsync(DlgAddressId, promptOptions, cancellationToken);
}
private async Task<DialogTurnResult> GetAddressStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
Form15DialogValues userInfo = (Form15DialogValues)stepContext.Values[UserInfo];
if (!(stepContext.Result is string[] results) || results.Length == 0)
{
return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
userInfo.JobNumber = null;
userInfo.StreetAddress = results[0];
userInfo.Suburb = null;
if (UpdateUserInfoFromWebService(userInfo))
{
stepContext.Values[UserInfo] = userInfo;
return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
await _userState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
await _conversationState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
var promptOptions = new PromptOptions { Prompt = MessageFactory.Text("I'm unable to find that address. Could you please enter the job number?") };
return await stepContext.PromptAsync(nameof(TextPrompt), promptOptions, cancellationToken);
}
I have been following the info in https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-dialog-manage-conversation-flow?view=azure-bot-service-4.0&tabs=csharp however I can not get "Dialog.RunAsync".
Any help/links/pointers you could give me would be most appreciated.
You can inject an instance of your dialog into the bot constructor like below
private readonly BotState _userState;
private readonly Dialog Dialog;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger, MilaAccessors accessors,Form15Dialog dialog)
{
_logger = logger;
_botServices = botServices;
_dialogSet = new DialogSet(accessors.ConversationDialogState);
_milaAccessors = accessors;
_userState = accessors.UserState;
Dialog = dialog;
}
Now you can run the below code in OnMessageActivityAsync to continue with your previous dialog
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
If you never call stepContext.EndDialogAsync WaterfallStep always finishes with the end of the Waterfall. In your code, it's DisplayForm15StepAsync and your dialog will be finished and goes back to OnMessageActivityAsync
So no need to reopen the active dialog, you should re-prompt at your WaterfallStep when it fails and doesn't let it finish until you get the right context

To route the QnAMaker to the hero card in the bot framework

I'm developing a chatbot using Microsoft Azure Web App Service. Among them, I'd like to insert a hero card in the greeting, and then click Info Dynamics365 among the corresponding hero buttons to start calling the card list source I've created, and if I press FAQ, I'd like to connect with QnAMaker for questioning. If you use ActionType now, both will call up a list of cards, one of which type should be used to connect to QnAMaker, or how to specify a path.
Bpts/QnABot.cs
namespace Microsoft.BotBuilderSamples
{
public class QnABot<T> : ActivityHandler
{
private BotState _conversationState;
private BotState _userState;
//QnAMaker
//protected readonly BotState ConversationState;
//protected readonly Microsoft.Bot.Builder.Dialogs.Dialog Dialog;
//protected readonly BotState UserState;
private string KBID = "fcd905a3-7269-4ea5-9a58-7b02c888ddb6";
private string ENDPOINT_KEY = "7b9a938a-e7ac-46f6-ab69-97d4e2e04f66";
private string HOST = "myfirstqa.azurewebsites.net";
//Azure 첫 세팅 소스
public QnABot(ConversationState conversationState, UserState userState)
{
_conversationState = conversationState;
_userState = userState;
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
var card = new HeroCard();
card.Title = "";
card.Text = #"Welcome to Welcome Users bot sample! This Introduction card";
card.Images = new List<CardImage>() { new CardImage("https://www.google.com/url?sa=i&source=images&cd=&ved=2ahUKEwjQjeeS4obmAhUIfnAKHQGgCB0QjRx6BAgBEAQ&url=https%3A%2F%2Fdougame.tistory.com%2F98&psig=AOvVaw11Y-BZJtsxh1pTp0Qxzedb&ust=1574819546545481") };
card.Buttons = new List<CardAction>()
{
new CardAction(ActionTypes.PostBack, "테스트 FAQ연결", null,"Connect QnA-Makeshfrjflrk todrur","Connect QnA-Maker", "Connection QnAMaker"),
new CardAction(ActionTypes.PostBack, "YOUTUBE LINK", null,"Connect YouTube","Connect YouTube", "Connection YouTube"),
new CardAction(ActionTypes.PostBack, "테스트 F연결", null,"Connect QnA-Maker","Connect QnA-Maker", "Connection A")
};
var response = MessageFactory.Attachment(card.ToAttachment());
await turnContext.SendActivityAsync(response, cancellationToken);
}
}
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
//QnA Maker연결
var qnaMaker = new QnAMaker(new QnAMakerEndpoint
{
KnowledgeBaseId = "fcd905a3-7269-4ea5-9a58-7b02c888ddb6",
EndpointKey = "7b9a938a-e7ac-46f6-ab69-97d4e2e04f66",
Host = "myfirstqa.azurewebsites.net"
},
null,
new System.Net.Http.HttpClient());
var conversationStateAccessors = _conversationState.CreateProperty<ConversationService>(nameof(ConversationService));
var conversationService = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationService());
var input = turnContext.Activity.Text;
if (String.IsNullOrEmpty(conversationService.currentService) && (input.Equals("Connection QnAMaker")))
{
conversationService.currentService = input;
await turnContext.SendActivityAsync(MessageFactory.Text("선택 : " + input + " service ,\n 입력할 내용 " + input + " question"), cancellationToken);
}
else if (String.IsNullOrEmpty(conversationService.currentService))
{
await turnContext.SendActivityAsync(MessageFactory.Text("select a service from hero card first"), cancellationToken);
}
else if (conversationService.currentService.Equals("Connection QnAMaker"))
{
//call your dy QNA service here
var result = qnaMaker.GetAnswersAsync(turnContext).GetAwaiter().GetResult();
if (result.Length == 0)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry , I can't find any answer for it"), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text(result[0].Answer), cancellationToken);
}
////await turnContext.SendActivityAsync(MessageFactory.Text(result[0].Answer), cancellationToken);
}
else if (conversationService.currentService.Equals("dy365"))
{
//call your dy 365 service here
await turnContext.SendActivityAsync(MessageFactory.Text("dy365 response"), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("error"), cancellationToken);
};
}
}
public class ConversationService
{
public string currentService { get; set; }
}
}
Dialog/RootDialog.cs
namespace Microsoft.BotBuilderSamples.Dialog
{
public class RootDialog : ComponentDialog
{
private const string InitialDialog = "initial-dialog";
public RootDialog(IBotServices services)
: base("root")
{
AddDialog(new QnAMakerBaseDialog(services));
AddDialog(new WaterfallDialog(InitialDialog)
.AddStep(InitialStepAsync));
// The initial child Dialog to run.
InitialDialogId = InitialDialog;
}
private async Task<DialogTurnResult> InitialStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Set values for generate answer options.
var qnamakerOptions = new QnAMakerOptions
{
ScoreThreshold = QnAMakerBaseDialog.DefaultThreshold,
Top = QnAMakerBaseDialog.DefaultTopN,
Context = new QnARequestContext()
};
// Set values for dialog responses.
var qnaDialogResponseOptions = new QnADialogResponseOptions
{
NoAnswer = QnAMakerBaseDialog.DefaultNoAnswer,
ActiveLearningCardTitle = QnAMakerBaseDialog.DefaultCardTitle,
CardNoMatchText = QnAMakerBaseDialog.DefaultCardNoMatchText,
CardNoMatchResponse = QnAMakerBaseDialog.DefaultCardNoMatchResponse
};
var dialogOptions = new Dictionary<string, object>
{
[QnAMakerBaseDialog.QnAOptions] = qnamakerOptions,
[QnAMakerBaseDialog.QnADialogResponseOptions] = qnaDialogResponseOptions
};
return await stepContext.BeginDialogAsync(nameof(QnAMakerBaseDialog), dialogOptions, cancellationToken);
}
}
}
The above is the part that selects the desired function at the same time as the greeting.
Try the code below which based on this official demo .Replace the content of Bots/StateManagementBot.cs with the code below :
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Builder.AI.QnA;
namespace Microsoft.BotBuilderSamples
{
public class StateManagementBot : ActivityHandler
{
private BotState _conversationState;
private BotState _userState;
private string KBID = "<KBID>";
private string ENDPOINT_KEY = "<KEY>";
private string HOST = "<QnA maker host>";
public StateManagementBot(ConversationState conversationState, UserState userState)
{
_conversationState = conversationState;
_userState = userState;
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
var card = new HeroCard();
card.Title = "";
card.Text = #"Welcome to Welcome Users bot sample! This Introduction card";
card.Images = new List<CardImage>() { new CardImage("https://www.google.com/url?sa=i&source=images&cd=&ved=2ahUKEwjQjeeS4obmAhUIfnAKHQGgCB0QjRx6BAgBEAQ&url=https%3A%2F%2Fdougame.tistory.com%2F98&psig=AOvVaw11Y-BZJtsxh1pTp0Qxzedb&ust=1574819546545481") };
card.Buttons = new List<CardAction>()
{
//new CardAction(ActionTypes.OpenUrl, "FAQ", null, "Get an overview", "Get an overview", "https://learn.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"),
new CardAction(ActionTypes.PostBack, "Info Dynamics365", null, "Ask a question", "Ask a question", "dy365"),
new CardAction(ActionTypes.PostBack, "FAQ",null , "Ask a question", "Ask a question", "FAQ" ),
new CardAction(ActionTypes.OpenUrl, "Connect", null, "Learn how to deploy", "Learn how to deploy", "https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0"),
};
var response = MessageFactory.Attachment(card.ToAttachment());
await turnContext.SendActivityAsync(response, cancellationToken);
}
}
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
var qnaMaker = new QnAMaker(new QnAMakerEndpoint
{
KnowledgeBaseId = KBID,
EndpointKey = ENDPOINT_KEY,
Host = HOST
},
null,
new System.Net.Http.HttpClient());
var conversationStateAccessors = _conversationState.CreateProperty<ConversationService>(nameof(ConversationService));
var conversationService = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationService());
var input = turnContext.Activity.Text;
if (String.IsNullOrEmpty(conversationService.currentService) && (input.Equals("FAQ") || input.Equals("dy365")))
{
conversationService.currentService = input;
await turnContext.SendActivityAsync(MessageFactory.Text("using "+ input + " service , pls enter your " + input + " question"), cancellationToken);
} else if (String.IsNullOrEmpty(conversationService.currentService)) {
await turnContext.SendActivityAsync(MessageFactory.Text("select a service from hero card first"), cancellationToken);
}
else if (conversationService.currentService.Equals("FAQ"))
{
var result = qnaMaker.GetAnswersAsync(turnContext).GetAwaiter().GetResult();
if (result.Length == 0)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry , I can't find any answer for it"), cancellationToken);
}
else {
await turnContext.SendActivityAsync(MessageFactory.Text(result[0].Answer), cancellationToken);
}
}
else if (conversationService.currentService.Equals("dy365"))
{
//call your dy 365 service here
await turnContext.SendActivityAsync(MessageFactory.Text("dy365 response"), cancellationToken);
}
else {
await turnContext.SendActivityAsync(MessageFactory.Text("error"), cancellationToken);
};
}
}
public class ConversationService{
public string currentService { get; set; }
}
}
You can find all QnA related params on QnA portal after you publish it :
Result :
In brief , let user select a service first and save the service type into conversation state so that users' requests will be redirected to corresponding service .
Hope it helps .

Dialog starts via interruption by Intents and QnaMaker Answer only when there is no active dialog

I don't know if i can explain this well but please bear with me. There are codes and images below.
So I am migrating my codes from the first version of Bot Framework V4 to the latest version.
I am trying to make a base for a bot that can call other dialog and cancel the current dialog anytime. and also answer questions with QnAMaker when there are no active dialogs.
There are no errors but the bot is not behaving as expected.
Expecting: When user first interracts with "Get Started" the main menu will be called because I added "Get started" to the intents of Main menu.
Actual Results: The main menu is being called twice.
Expecting: When i call DialogA from interruption via intent. The dialogA will be called and if there is any active dialog it will be cancelled.
Actual Results: Dialog A is called and current active dialog is ended BUT dialogA also ends abruptly.(Even when you did not answer its choice prompt yet).
Note: when i call dialogA via choice prompt in the main menu. the dialog A is starting normally and not ending.
Expecting: when there are no active dialog(Example if you cancel dialogs) user can ask a question and the bot check for answers in the QnaMaker. Actual results: the bot answer the question then start main menu. and even when there are active dialog the bot still answer the questions.
Here are the codes:
DialogBot:
namespace SabikoBotV2.Bots
{
public class DialogBot<T> : ActivityHandler
where T : Dialog
{
public readonly IStatePropertyAccessor<DialogState> _dialogAccessor;
protected readonly Dialog Dialog;
protected readonly BotState ConversationState;
protected readonly BotState UserState;
protected readonly ILogger Logger;
private readonly IBotServices BotServices;
private DialogSet Dialogs { get; set; }
public DialogBot(IBotServices botServices, ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger)
{
ConversationState = conversationState;
UserState = userState;
Dialog = dialog;
Logger = logger;
BotServices = botServices;
Dialogs = new DialogSet(conversationState.CreateProperty<DialogState>(nameof(DialogBot<T>)));
RegisterDialogs(Dialogs);
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Running dialog with Message Activity.");
string text = string.IsNullOrEmpty(turnContext.Activity.Text) ? string.Empty : turnContext.Activity.Text.ToLower();
string topIntent = string.Empty;
RecognizerResult luisRecognizerResult = null;
string topDispatch = string.Empty;
RecognizerResult dispatchRecognizerResult = null;
if (!string.IsNullOrEmpty(text))
{
dispatchRecognizerResult = await BotServices.DispatchService.RecognizeAsync(turnContext, cancellationToken);
var topScoringDispatch = dispatchRecognizerResult?.GetTopScoringIntent();
topDispatch = topScoringDispatch.Value.intent;
luisRecognizerResult = await BotServices.LuisService.RecognizeAsync(turnContext, cancellationToken);
var topScoringIntent = luisRecognizerResult?.GetTopScoringIntent();
topIntent = topScoringIntent.Value.intent;
turnContext.TurnState.Add("topDispatch", topDispatch);
turnContext.TurnState.Add("dispatchRecognizerResult", dispatchRecognizerResult);
turnContext.TurnState.Add("botServices", BotServices);
turnContext.TurnState.Add("topIntent", topIntent);
}
var dc = await Dialogs.CreateContextAsync(turnContext, cancellationToken);
var dialogResult = await dc.ContinueDialogAsync();
if (!dc.Context.Responded)
{
switch (dialogResult.Status)
{
case DialogTurnStatus.Empty:
await DispatchToTopIntentAsync(turnContext, topDispatch, dispatchRecognizerResult, cancellationToken);
break;
case DialogTurnStatus.Waiting:
break;
case DialogTurnStatus.Complete:
await dc.EndDialogAsync();
break;
default:
await dc.CancelAllDialogsAsync();
break;
}
}
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
private void RegisterDialogs(DialogSet dialogs)
{
dialogs.Add(new MainDialog());
dialogs.Add(new DialogA());
dialogs.Add(new DialogB());
}
private async Task DispatchToTopIntentAsync(ITurnContext turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case QnAModel:
await DispatchToQnAMakerAsync(turnContext, cancellationToken);
break;
}
}
private async Task DispatchToQnAMakerAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(turnContext.Activity.Text))
{
var results = await BotServices.QnaService.GetAnswersAsync(turnContext);
if (results.Any())
{
await turnContext.SendActivityAsync(MessageFactory.Text(results.First().Answer), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, could not find an answer in the Q and A system."), cancellationToken);
}
}
}
}
}
startup
namespace SabikoBotV2
{
public class Startup
{
public Startup()
{
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
services.AddSingleton<IStorage, MemoryStorage>();
services.AddSingleton<UserState>();
services.AddSingleton<ConversationState>();
services.AddSingleton<IBotServices, BotServices>();
services.AddTransient<MainDialog>();
services.AddTransient<DialogA>();
services.AddTransient<DialogB>();
services.AddTransient<IBot, DialogBot<MainDialog>>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}
}
}
CancelAndHelpDialog
namespace SabikoBotV2.DialogsV2
{
public class CancelAndHelpDialog : ComponentDialog
{
public CancelAndHelpDialog(string id)
: base(id)
{
}
protected override async Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default(CancellationToken))
{
var result = await IsTurnInterruptedAsyncHelpAndCancel(innerDc, cancellationToken);
if (result != null)
{
return result;
}
return await base.OnBeginDialogAsync(innerDc, options, cancellationToken);
}
protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
var result = await IsTurnInterruptedAsyncHelpAndCancel(innerDc, cancellationToken);
if (result != null)
{
return result;
}
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}
protected virtual async Task<DialogTurnResult> IsTurnInterruptedAsyncHelpAndCancel(DialogContext innerDc, CancellationToken cancellationToken)
{
var topIntent = innerDc.Context.TurnState.Get<string>("topIntent");
var text = innerDc.Context.TurnState.Get<string>("text");
if (topIntent.Equals("Cancel"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.Context.SendActivityAsync("👍 Ok. I've cancelled our last activity.");
}
else
{
await innerDc.Context.SendActivityAsync("I don't have anything to cancel.");
}
}
if (topIntent.Equals("Help"))
{
await innerDc.Context.SendActivityAsync("Let me help you");
if (innerDc.ActiveDialog != null)
{
await innerDc.RepromptDialogAsync();
}
}
if (topIntent.Equals("MainDialog"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
else
{
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
}
if (topIntent.Equals("DialogA"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(DialogA));
}
else
{
await innerDc.BeginDialogAsync(nameof(DialogA));
}
}
if (topIntent.Equals("DialogB"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(DialogB));
}
else
{
await innerDc.BeginDialogAsync(nameof(DialogB));
}
}
return null;
}
}
}
MainDialog
namespace SabikoBotV2.Dialogs
{
public class MainDialog : CancelAndHelpDialog
{
private const string InitialId = nameof(MainDialog);
public MainDialog()
: base(nameof(MainDialog))
{
InitialDialogId = InitialId;
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
ThirdStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new DialogA());
AddDialog(new DialogB());
}
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync("Start of Main");
return await stepContext.PromptAsync(
nameof(ChoicePrompt),
new PromptOptions
{
Prompt = MessageFactory.Text($"What do you want to do next?"),
Choices = new List<Choice>
{
new Choice
{
Value = "choice1",
},
new Choice
{
Value = "choice2",
},
},
RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
});
}
private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
switch ((stepContext.Result as FoundChoice).Value.ToString().ToLower())
{
case "choice1":
return await stepContext.BeginDialogAsync(nameof(DialogA));
case "choice2":
return await stepContext.BeginDialogAsync(nameof(DialogB));
default:
return await stepContext.ReplaceDialogAsync(nameof(MainDialog));
}
}
private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
await stepContext.Context.SendActivityAsync("End of Main");
return await stepContext.EndDialogAsync();
}
}
}
DialogA and DialogB are same with maindialog but they inherit ComponentDialog instead of CancelAndHelpDialog.
some screenshot for reference:
when dialogA is called via choice of main menu it starts normally
dialogA is called via intent interruption on it end abruptly.
qna maker anwering questions even if there is an active dialog
Hello I managed to fix my problem. For future reference of others here is how i fixed it.
I fixed the running of dialog via intent interruption by adding this line in the cancelandhelpdialog
return new DialogTurnResult(DialogTurnStatus.Waiting);
if (topIntent.Equals("MainDialog"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
else
{
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
and the QnaMaker to only answer when there are no active dialog by doing this in the OnTurnAsync:
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
var topDispatch = turnContext.TurnState.Get<string>("topDispatch");
var dispatchRecognizerResult = turnContext.TurnState.Get<RecognizerResult>("dispatchRecognizerResult");
var dc = await Dialogs.CreateContextAsync(turnContext, cancellationToken);
var dialogResult = await dc.ContinueDialogAsync();
if (!dc.Context.Responded)
{
switch (dialogResult.Status)
{
case DialogTurnStatus.Empty:
await DispatchToTopIntentAsync(turnContext, topDispatch, dispatchRecognizerResult, cancellationToken);
break;
case DialogTurnStatus.Waiting:
break;
case DialogTurnStatus.Complete:
await dc.EndDialogAsync();
break;
default:
await dc.CancelAllDialogsAsync();
break;
}
}
// Save any state changes that might have occured during the turn.
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}
Hope this will help somebody.

Stackoverflow Exception in a simple dialog

Hello i am getting an Stackoverflow Exception in this two dialog. Dialog A is being called from a main dialog class. Dialog A have a choice to go to Dialog A child and Dialog A child has a choice to go back to Dialog A. But it is getting a Stackoverflow Exception. When i remove one from the other: Example removing Dialog A child from Dialog A or removing Dialog A from Dialog A child, the exception error disappears. In short it throw a Stackoverflow Exception when both dialog can call each other.
I know i can just EndDialogAsync in this specific scenario to go back to Dialog A but my real dialog flow is not together like this
. How to fix this?
Dialog A code:
public class DialogA : ComponentDialog
{
private const string InitialId = "dialogA";
private const string ChoicePrompt = "choicePrompt";
private const string DialogAchildId = "dialogA_childId";
public DialogA(string dialogId)
: base(dialogId)
{
InitialDialogId = InitialId;
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
ThirdStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new ChoicePrompt(ChoicePrompt));
AddDialog(new DialogA_child(DialogAchildId));
}
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.PromptAsync(
ChoicePrompt,
new PromptOptions
{
Prompt = MessageFactory.Text($"Here are your choices:"),
Choices = new List<Choice>{new Choice { Value = "Open Dialog A_Child", }, new Choice { Value = "Open Dialog B_Child" }, },
RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
});
}
private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
var response = (stepContext.Result as FoundChoice)?.Value.ToLower();
if (response == "open dialog a_child")
{
return await stepContext.BeginDialogAsync(DialogAchildId, cancellationToken: cancellationToken);
}
return await stepContext.NextAsync();
}
private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.EndDialogAsync();
}
Dialog A child code:
public class DialogA_child : ComponentDialog
{
private const string InitialId = "dialogAchild";
private const string ChoicePrompt = "choicePrompt";
private const string DialogAId = "dialogAId";
public DialogA_child(string dialogId)
: base(dialogId)
{
InitialDialogId = InitialId;
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new DialogA(DialogAId));
}
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.PromptAsync(
ChoicePrompt,
new PromptOptions
{
Prompt = MessageFactory.Text($"Here are your choices:"),
Choices = new List<Choice> {new Choice { Value = "Open Dialog A" }, new Choice { Value = "Open Dialog B" }, },
RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
});
}
private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
var response = (stepContext.Result as FoundChoice)?.Value.ToLower();
if (response == "open dialog a")
{
return await stepContext.BeginDialogAsync(DialogAId, cancellationToken: cancellationToken);
}
return await stepContext.NextAsync();
}
Main code that called dialog A:
public class MainDialog : ComponentDialog
{
private const string InitialId = "mainDialog";
private const string ChoicePrompt = "choicePrompt";
private const string DialogAId = "dialogAId";
public MainDialog(string dialogId)
: base(dialogId)
{
InitialDialogId = InitialId;
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
ThirdStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new ChoicePrompt(ChoicePrompt));
AddDialog(new DialogA(DialogAId));
}
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.PromptAsync(
ChoicePrompt,
new PromptOptions
{
Prompt = MessageFactory.Text($"Here are your choices:"),
Choices = new List<Choice>{ new Choice { Value = "Open Dialog A" }, new Choice { Value = "Open Dialog B" }, },
RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
});
}
private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
var response = (stepContext.Result as FoundChoice)?.Value.ToLower();
if (response == "open dialog a")
{
return await stepContext.BeginDialogAsync(DialogAId, cancellationToken: cancellationToken);
}
if (response == "open dialog b")
{
}
return await stepContext.NextAsync();
}
private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.EndDialogAsync();
}
In Visual Studio you can check your call stack and you will know where is the exact problem in your StackOverflowException.
If DialogA is the base class of DialogA_child then in your DialogA_child's constructor and your base class constructor will be calling themselfs recursively.
So your call stack should look like this:
DialogA constructor add new DialogA_child
DialogA_child calls base(so DialogA constructor)
DialogA constructor add new DialogA_child
DialogA_child calls base(so DialogA constructor)
...
#koviroli's answer is 100% correct, so please accept his as the answer once you feel like you understand it. I'm adding this as an additional answer because it seems like you're struggling to understand things a little and comments limit me from providing good explanation.
Quick Explanation of Constructors
Since you're new to C#, I'll provide a quick explanation of constructors. DialogA_child's constructor is this part:
public DialogA_child(string dialogId)
: base(dialogId)
{
InitialDialogId = InitialId;
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new DialogA(DialogAId));
}
Any time that you use new DialogA_child("xyz"), the constructor is called to "construct" DialogA_child. The :base(dialogId) makes it so that "xyz" gets sent to the constructor of DialogA_child's base class, which is ComponentDialog. In ComponentDialog's constructor, it sets the argument that gets passed in ("xyz", in this case) to the dialogId.
If you click on ComponentDialog in your code and press F12, it will take you to the definition of ComponentDialog and you can see this:
public ComponentDialog(string dialogId);.
Here's What's Wrong
In DialogA_child's constructor, you have: AddDialog(new DialogA(DialogAId));, which creates a new instance of DialogA. Then, in DialogA's constructor, you have AddDialog(new DialogA_child(DialogAchildId));, which create another DialogA_child, and so on and so on.
Basically, DialogA and DialogA_child keep creating new instances of each other, causing the StackOverflow.
The easiest fix is to just remove AddDialog(new DialogA(DialogAId));.
Additional Notes
Again, I know you're new to C#, so I'll help you out with a couple of other things.
private const string ChoicePrompt = "choicePrompt";
should probably be
private const string ChoicePromptId = "choicePrompt";
since ChoicePrompt is already defined as a type of prompt.
When defining your dialog constructors, it's easiest to use something like:
public DialogA() : base(nameof(DialogA))
This will automatically set the id of DialogA to "DialogA". It will help with two things:
Since dialogs have to use unique IDs, this will prevent you from accidentally calling the same dialog twice.
It's easier to keep track of and you don't have to pass in a name for it. For example, to call the dialog, you could now use AddDialog(new DialogA()); instead of AddDialog(new DialogA(DialogAId));.
Forcing a Dialog Loop
Currently, you cannot loop dialogs the way that you want to (See update below). You cannot:
Have DialogA call DialogA_child
Then have DialogA_child again call DialogA.
As you have seen, this creates a stack overflow.
Instead, you can call it indirectly.
Instead of having DialogA_child call DialogA, do something like:
Have DialogA_child's choice prompt with an option of "Restart Dialog A" (or something unique).
In OnTurnAsync (in your bot's main Class file), check to see if the user responded with "Restart Dialog A". If so, clear all dialogs (or just replace) and then begin DialogA again.
Code:
DialogA_child:
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.PromptAsync(
choicePrompt,
new PromptOptions
{
Prompt = MessageFactory.Text($"Here are your choices:"),
Choices = new List<Choice> { new Choice { Value = "Restart Dialog A" }, new Choice { Value = "Open Dialog B" }, },
RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
});
}
<myBot>.cs:
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
var activity = turnContext.Activity;
var dc = await Dialogs.CreateContextAsync(turnContext);
if (activity.Text == "Restart Dialog A")
{
await dc.CancelAllDialogsAsync();
await dc.BeginDialogAsync(nameof(DialogA));
}
Update
BotBuilder SDK V4.3 will release soon that allows any child or sibling dialog to call any dialog defined by the parent. See this pull request. I believe you can have a child dialog call a parent, as OP requested, but it's still new and I haven't tested.

Categories