Botframework v4 same state for all users C# - c#

I'm trying to migrate my botframework APP v3 to v4, I followed the instruction from microsoft documentation and everything works fine except state control, in my bot the state is shared for all user and for all channels (directline, telegram).
I saw this topic UserProfile state persistent between users in bot v4 but even forcing web random ID the state still share between users.
My bot has been created with Core bot template provided by Microsoft. You can see my C# code below. I'll skip to post Maindialog due to only business rules there in a waterfall dialog, but if needed I can post too.
Thanks for the help.
Startup.cs
public class Startup
{
private const string BotOpenIdMetadataKey = "BotOpenIdMetadata";
private static readonly AzureBlobStorage _UserStorage = new AzureBlobStorage("DefaultEndpointsProtocol=https;AccountName=evabotwebbe9c;AccountKey=wsU18jImJVSX+2vq6l0flx9Ou83hcDyrxie0tUN7fjxMV3bfHhYJuFobmq0h/TXU/pBBOvfpGVUlHtuqn7cNVw==;EndpointSuffix=core.windows.net", "botuserstate");
private static readonly AzureBlobStorage _ConversationStorage = new AzureBlobStorage("DefaultEndpointsProtocol=https;AccountName=evabotwebbe9c;AccountKey=wsU18jImJVSX+2vq6l0flx9Ou83hcDyrxie0tUN7fjxMV3bfHhYJuFobmq0h/TXU/pBBOvfpGVUlHtuqn7cNVw==;EndpointSuffix=core.windows.net", "botconversationstate");
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
if (!string.IsNullOrEmpty(Configuration[BotOpenIdMetadataKey]))
ChannelValidation.OpenIdMetadataUrl = Configuration[BotOpenIdMetadataKey];
// Create the credential provider to be used with the Bot Framework Adapter.
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
// Create the Bot Framework Adapter with error handling enabled.
services.AddSingleton<IBotFrameworkHttpAdapter, WebSocketEnabledHttpAdapter>();
// Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
services.AddSingleton<IStorage, AzureBlobStorage>();
// Create the User state. (Used in this bot's Dialog implementation.)
var userState = new UserState(_UserStorage);
// Create the Conversation state. (Used by the Dialog system itself.)
var conversationState = new ConversationState(_ConversationStorage);
// Add the states as singletons
services.AddSingleton(userState);
services.AddSingleton(conversationState);
services.AddSingleton(sp =>
{
userState = sp.GetService<UserState>();
conversationState = sp.GetService<ConversationState>();
// Create the Conversation state. (Used by the Dialog system itself.)
return new BotStateSet(userState, conversationState);
});
// The Dialog that will be run by the bot.
services.AddSingleton<MainDialog>();
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddTransient<IBot, DialogAndWelcomeBot<MainDialog>>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseWebSockets();
//app.UseHttpsRedirection();
app.UseMvc();
}
}
DialogAndWelcomeBot.cs
public class DialogAndWelcomeBot<T> : DialogBot<T> where T : Dialog
{
protected IConfiguration Configuration;
private BotState _conversationState;
private BotState _userState;
protected IStatePropertyAccessor<DialogState> accessor;
protected Dialogo dialogo;
protected Dialog Dialog;
public DialogAndWelcomeBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger, IConfiguration configuration)
: base(conversationState, userState, dialog, logger)
{
Configuration = configuration;
dialogo = new Dialogo();
_conversationState = conversationState;
_userState = userState;
Dialog = dialog;
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
// Greet anyone that was not the target (recipient) of this message.
// To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details.
if (member.Id != turnContext.Activity.Recipient.Id)
{
dialogo = await VerificarDialogo();
await IniciarDialogo(turnContext, cancellationToken);
}
}
}
private async Task IniciarDialogo(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
accessor = _conversationState.CreateProperty<DialogState>("DialogState");
var dialog = Dialog;
var dialogSet = new DialogSet(accessor);
dialogSet.Add(dialog);
var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
if (results.Status == DialogTurnStatus.Empty)
{
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());
await dialogContext.BeginDialogAsync(dialog.Id, dialogo, cancellationToken);
}
}
DialogBot.cs
public class DialogBot<T> : ActivityHandler where T : Dialog
{
private readonly Dialog Dialog;
private BotState _conversationState;
private BotState _userState;
protected readonly ILogger Logger;
public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger)
{
_conversationState = conversationState;
_userState = userState;
Dialog = dialog;
Logger = logger;
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Client notifying this bot took to long to respond (timed out)
if (turnContext.Activity.Code == EndOfConversationCodes.BotTimedOut)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Desculpe-me, mas parece que a conexão com a internet está ruim e isto irá afetar o desempenho da nossa conversa."), cancellationToken);
return;
}
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Message Activity log");
// Run the Dialog with the new message Activity.
await Dialog.Run(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
}
DialogExtensions.cs
public static class DialogExtensions
{
public static async Task Run(this Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken = default(CancellationToken))
{
var dialogSet = new DialogSet(accessor);
dialogSet.Add(dialog);
var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
if (results.Status == DialogTurnStatus.Empty)
{
await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken);
}
else
{
await dialogContext.ReplaceDialogAsync(dialog.Id, null, cancellationToken);
}
}
}
Jscript code into HTML file.
<script>
function guid() {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
var user = {
id: guid().toUpperCase(),
name: 'User-' + Math.floor((1 + Math.random()) * 10000)
};
var botConnection = new BotChat.DirectLine({
token: 'MyToken',
user: user,
webSocket: 'true'
});
const speechOptions = {
speechRecognizer: new BotChat.Speech.BrowserSpeechRecognizer(),
speechSynthesizer: new BotChat.Speech.BrowserSpeechSynthesizer()
};
BotChat.App({
user: user,
botConnection: botConnection,
speechOptions: speechOptions,
bot: { id: 'Bot'+ Math.floor((1 + Math.random()) * 10000)},
resize: 'detect',
}, document.getElementById("bot"));
botConnection
.postActivity({
from: user,
name: 'none',
type: 'event',
value: 'none'
})
.subscribe(function (id) {
console.log('"trigger setUserIdEvent" sent');
});
</script>

What is _UserStorage? ... the UserState class should be utilizing your IStorage implementation, which it will do automatically since you are already injecting an IStorage If you just inject the UserState, di will ensure it is created using the IStorage already present: services.AddSingleton<UserState>();
There is not enough here to determine what is going wrong.
Also, what is happening here?
if (member.Id != turnContext.Activity.Recipient.Id)
{
dialogo = await VerificarDialogo();
await IniciarDialogo(turnContext, cancellationToken);
}

I found the problem, it was logical and not in the code, the problem is my global variable, in net core they are "common" for all instances so all users from diferent instances got the same values in my global variables.

Related

Bot Framework AdaptiveDialog: Only trigger activity if the dialog is on top of the stack?

I have the following "AdaptiveDialog"-style root dialog in Bot Framework v4:
public class CustomRootDialog : AdaptiveDialog
{
public CustomRootDialog() : base(nameof(CustomRootDialog))
{
Recognizer = new RegexRecognizer()
{
Intents = new List<IntentPattern>()
{
new IntentPattern() { Intent = "HelpIntent", Pattern = "(?i)help" },
new IntentPattern() { Intent = "SecondaryDialog", Pattern = "(?i)run" },
},
};
Triggers.Add(new OnIntent("HelpIntent", actions: new List<Dialog>()
{
new SendActivity("Hi, here's the help from the root!")
}));
Triggers.Add(new OnIntent("SecondaryDialog", actions: new List<Dialog>()
{
new SecondaryDialog()
}));
}
}
When user starts a conversation with the bot and says "help", the dialog answers as expected:
I also have an another dialog which can be started using the "run"-keyword. Here's the another dialog:
public class SecondaryDialog : Dialog
{
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = new CancellationToken())
{
var input = dc.Context.Activity.Text;
if (input == "help")
{
await dc.Context.SendActivityAsync("Secondary dialog help", cancellationToken: cancellationToken);
return EndOfTurn;
}
if (input == "exit")
{
return new DialogTurnResult(DialogTurnStatus.Complete);
}
return EndOfTurn;
}
}
Now when user moves to the second dialog using "run" and then types "help", both the dialogs answer:
I expected that only the secondary dialog would answer. I can see from the secondary dialog that its Context.Responsed is true, which means I could skip the SendActivity from the secondary dialog. But I want to do the opposite: the root dialog should answer only if the dialog currently on top of the stack hasn't answered.
Is it possible to modify the AdaptiveDialog's trigger so that it only triggers if either no other dialog has responded or if there are no other dialogs running?
Update:
The bot is created using the guide provided by Microsoft's docs here: https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-adaptive-dialog-setup?view=azure-bot-service-4.0
Here's how the bot is registered:
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
ComponentRegistration.Add(new DialogsComponentRegistration());
ComponentRegistration.Add(new AdaptiveComponentRegistration());
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
services.AddSingleton<IStorage, MemoryStorage>();
services.AddSingleton<UserState>();
services.AddSingleton<ConversationState>();
services.AddSingleton<IBot, DialogBot<CustomRootDialog>>();
...
public class DialogBot<T> : ActivityHandler
where T : Dialog
{
private readonly DialogManager DialogManager;
protected readonly ILogger Logger;
public DialogBot(T rootDialog, ILogger<DialogBot<T>> logger)
{
Logger = logger;
DialogManager = new DialogManager(rootDialog);
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
Logger.LogInformation("Running dialog with Activity.");
await DialogManager.OnTurnAsync(turnContext, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}

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 .

The given key 'dialogs' was not present in the dictionary while using ContinueDialogAsync

I am using Bot Framework SDK v-4. I have created a couple of dialogs inherited from ComponentDialog and overridden the methods: BeginDialogAsync and ContinueDialogAsync. Below is the implementation of the IBot.
public class Bot : IBot
{
private readonly BotAccessors _accessors;
private readonly ILogger _logger;
private DialogSet _dialogs = null;
private string _botName = string.Empty;
private IConfiguration _configuration = null;
private UserDetail _userDetail = null;
private ConversationData _conversationData = null;
private IStatePropertyAccessor<TurnState> _turnStateAccessor = null;
/// <summary>
/// Initializes a new instance of the class.
/// </summary>
public Bot(BotAccessors accessors, ILoggerFactory loggerFactory, IConfiguration configuration)
{
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
if (loggerFactory == null)
{
throw new System.ArgumentNullException(nameof(loggerFactory));
}
_configuration = configuration;
_dialogs = new DialogSet(accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState)));
_turnStateAccessor = accessors.TurnStateAccessor;
_dialogs.Add(new GreetingDialog(_turnStateAccessor, loggerFactory, configuration));
_dialogs.Add(new QnADialog(_turnStateAccessor, loggerFactory, configuration));
_botName = configuration["BotName"];
_logger = loggerFactory.CreateLogger<Bot>();
_logger.LogTrace("Bot turn start.");
}
public override Task<DialogTurnResult> BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
_turnState = _turnStateAccessor.GetAsync(outerDc.Context).Result;
outerDc.ContinueDialogAsync();
return base.BeginDialogAsync(outerDc, options, cancellationToken);
}
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken = default(CancellationToken))
{
_turnState = _turnStateAccessor.GetAsync(outerDc.Context).Result;
//some code
_turnStateAccessor.SetAsync(outerDc.Context, _turnState).ConfigureAwait(false);
return base.ContinueDialogAsync(outerDc, cancellationToken);
}
}
I call the dialog from the OnTurnAsync method of the Bot like this: await dialogContext.BeginDialogAsync(nameof(GreetingDialog)).ConfigureAwait(false);. It reaches the BeginDialogAsync method of my dialog and then continues to ContinueDialogAsync. It works fine, however, while returning (using base.ContinueDialogAsync(outerDc, cancellationToken);) from the method, I get some exception which I captured from the Diagnostic tool of the Visual Studio.
Also, I am sending the exception as an activity message to the client (bot framework emulator) which is shown below.
Sorry, it looks like something went wrong. Exception:
System.ArgumentNullException: Value cannot be null. Parameter name:
dialogId at
Microsoft.Bot.Builder.Dialogs.DialogContext.BeginDialogAsync(String
dialogId, Object options, CancellationToken cancellationToken) in
D:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\DialogContext.cs:line
84 at
Microsoft.Bot.Builder.Dialogs.ComponentDialog.BeginDialogAsync(DialogContext
outerDc, Object options, CancellationToken cancellationToken) in
D:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\ComponentDialog.cs:line
59 at
Microsoft.Bot.Builder.Dialogs.DialogContext.BeginDialogAsync(String
dialogId, Object options, CancellationToken cancellationToken) in
D:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\DialogContext.cs:line
84 at Chatbot.Bot.OnTurnAsync(ITurnContext turnContext,
CancellationToken cancellationToken) in
C:\GIS\ChatbotNew\Chatbot\Chatbot\Bot.cs:line 120 at
Microsoft.Bot.Builder.MiddlewareSet.ReceiveActivityWithStatusAsync(ITurnContext
turnContext, BotCallbackHandler callback, CancellationToken
cancellationToken) in
D:\a\1\s\libraries\Microsoft.Bot.Builder\MiddlewareSet.cs:line 55 at
Microsoft.Bot.Builder.BotAdapter.RunPipelineAsync(ITurnContext
turnContext, BotCallbackHandler callback, CancellationToken
cancellationToken) in
D:\a\1\s\libraries\Microsoft.Bot.Builder\BotAdapter.cs:line 167
Update-1
Based on Drew's answer I removed outerDc.ContinueDialogAsync(); but I get some error (shown below) while stepping through the return base.BeginDialogAsync(outerDc, options, cancellationToken); in BeginDialogAsync function.
Also, while stepping through the OnTurnAsync function I get an error while trying to get the BotState as shown below.
TurnState implementation:
public class TurnState
{
public int TurnCount { get; set; } = 0;
public string BotType { get; set; } = string.Empty;
public string ConversationLanguage { get; set; } = string.Empty;
//other properties...
}
Similar error while trying to create the DialogContext as well.
ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<Bot>(options =>
{
var secretKey = Configuration.GetSection("botFileSecret")?.Value;
// Loads .bot configuration file and adds a singleton that your Bot can access through dependency injection.
var botConfig = BotConfiguration.Load(#".\Chatbot.bot", secretKey);
services.AddSingleton(sp => botConfig);
// Retrieve current endpoint.
var service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == "development").FirstOrDefault();
if (!(service is EndpointService endpointService))
{
throw new InvalidOperationException($"The .bot file does not contain a development endpoint.");
}
//options.CredentialProvider = new SimpleCredentialProvider(Configuration[MicrosoftAppCredentials.MicrosoftAppIdKey], Configuration[MicrosoftAppCredentials.MicrosoftAppPasswordKey]);
options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
// Creates a logger for the application
ILogger logger = _loggerFactory.CreateLogger<Bot>();
// Catches any errors that occur during a conversation turn and logs them.
options.OnTurnError = async (context, exception) =>
{
await context.SendActivityAsync("Sorry, it looks like something went wrong. Exception: " + exception);
};
// The Memory Storage used here is for local bot debugging only. When the bot
// is restarted, everything stored in memory will be gone.
IStorage dataStore = new MemoryStorage();
var conversationState = new ConversationState(dataStore);
options.State.Add(conversationState);
});
// Create and register state accesssors.
// Acessors created here are passed into the IBot-derived class on every turn.
services.AddSingleton<BotAccessors>(sp =>
{
var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
if (options == null)
{
throw new InvalidOperationException("BotFrameworkOptions must be configured prior to setting up the state accessors");
}
var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
if (conversationState == null)
{
throw new InvalidOperationException("ConversationState must be defined and added before adding conversation-scoped state accessors.");
}
// Create the custom state accessor.
// State accessors enable other components to read and write individual properties of state.
var accessors = new BotAccessors(conversationState)
{
TurnStateAccessor = conversationState.CreateProperty<TurnState>(BotAccessors.TurnStateName),
};
return accessors;
});
}
Any help with this please?
I'm trying to wrap my head around exactly what you're expecting to happen here:
public override Task<DialogTurnResult> BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
_turnState = _turnStateAccessor.GetAsync(outerDc.Context).Result;
outerDc.ContinueDialogAsync();
return base.BeginDialogAsync(outerDc, options, cancellationToken);
}
Specifically, why are you calling outerDc.ContinueDialogAsync() here? That is basically tying the dialog stack up in a knot. If you removed that line everything else you show here should work perfectly.
If you update your question with some more details on exactly what you were trying to accomplish by calling that I'll happily update my answer to try and set you on the right path for accomplishing whatever that might be.

Categories