How to pass the prompt values from maindialog to dispatcher? - botframework

`namespace Microsoft.BotBuilderSamples
{
public class DispatchBot : ActivityHandler
{
private ILogger _logger;
private IBotServices _botServices;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger)
{
_logger = logger;
_botServices = botServices;
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// 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.
await DispatchToTopIntentAsync(turnContext, topIntent.intent, recognizerResult, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
const string WelcomeText = "I am here to make your bot experience much more easier";
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"Hi {member.Name}, I am your IT assistant at your service . {WelcomeText}"), cancellationToken);
}
}
}
private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case "l_us_bot":
await ProcessHomeAutomationAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "t_abs-bot":
await ProcessSampleQnAAsync(turnContext, cancellationToken);
break;
default:
_logger.LogInformation($"Dispatch unrecognized intent: {intent}.");
await turnContext.SendActivityAsync(MessageFactory.Text($"Dispatch unrecognized intent: {intent}."), cancellationToken);
break;
}
}
private Activity CreateResponse(IActivity activity, Attachment attachment)
{
var response = ((Activity)activity).CreateReply();
response.Attachments = new List<Attachment>() { attachment };
return response;
}
private async Task ProcessHomeAutomationAsync(ITurnContext<IMessageActivity> turnContext, LuisResult luisResult, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessHomeAutomationAsync");
// Retrieve LUIS result for Process Automation.
var result = luisResult.ConnectedServiceResult;
var topIntent = result.TopScoringIntent.Intent;
var entity = result.Entities;
if (topIntent == "welcome")
{
await turnContext.SendActivityAsync(MessageFactory.Text("Hi,This is your IT assistant"), cancellationToken);
}
if (topIntent == "None")
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry I didnt get you!"), cancellationToken);
}
if (topIntent == "DateTenure")
{
// Here i want to call my dialog class
}
}
}
private async Task ProcessSampleQnAAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessSampleQnAAsync");
var results = await _botServices.SampleQnA.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);
}
}
}
`I have a scenario as described:
When user says hi, the bot shows the adaptive cards (with Action.Submit on each card). ----Suppose the cards are as: 1. Book tickets 2. Need Help
3. Cancel
whenever user click any of the option ,i get the same user clicked value. Like
If the user selects "Need help". I get the same value..
then i am getting input from user like any question(query).
Then this query will go to the specific intent from dispatcher but its not going to intent from dispatcher and showing error (object reference not set to an instance of an object)
so what should i do?
for reference i already gone through this link "How to retrieve user entered input in adaptive card to the c# code and how to call next intent on submit button click"
it should take value from the dispatcher bot after user input.

Related

Log additional information with TranscriptLoggerMiddleware

I'm using TranscriptLoggerMiddleware to log transcript to Azure blobs.
Now, I want to add additional information to the activity, for example, account ID.
Ideally I want the account ID to be the top level folder when creating the blobs so one can easily locate all conversations for a given account.
The logger is only passed the activity without any context. So I'm looking at the Entities activity property which I can potentially use for storing my account ID.
Is this a valid approach?
Any other ideas on how to implement this?
Answering my own question.
This worked for me:
public class SetEntityMiddleware : IMiddleware
{
private readonly BotState _userState;
private readonly IStatePropertyAccessor<UserProfileState> _userProfileState;
public SetEntityMiddleware(UserState userState)
{
_userState = userState;
_userProfileState = userState.CreateProperty<UserProfileState>(nameof(UserProfileState));
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
{
var userProfile = await _userProfileState.GetAsync(turnContext, () => new UserProfileState(), cancellationToken);
this.SetEntity(turnContext.Activity, userProfile);
turnContext.OnSendActivities(async (ctx, activities, nextSend) =>
{
var userProfile = await _userProfileState.GetAsync(ctx, () => new UserProfileState(), cancellationToken);
foreach (var activity in activities)
{
this.SetEntity(activity, userProfile);
}
return await nextSend().ConfigureAwait(false);
});
await next(cancellationToken).ConfigureAwait(false);
}
private void SetEntity(Activity activity, UserProfileState userProfile)
{
if (activity.Type == ActivityTypes.Message &&
!string.IsNullOrEmpty(userProfile.AccountNumber))
{
var entity = new Entity();
entity.Type = "userProfile";
entity.Properties["accountNumber"] = userProfile.AccountNumber;
if (activity.Entities == null)
{
activity.Entities = new List<Entity>();
}
activity.Entities.Add(entity);
}
}
}

Dialog Continuation Issue on Bot Framework V4

I want to start a user dialog right after a welcome message has been displayed in my bot - without any initial user interaction.
Code snippet to accomplish that:
public RootDialogBot(BotServices services, BotAccessors accessors, ILoggerFactory loggerFactory)
{
if (loggerFactory == null)
{
throw new System.ArgumentNullException(nameof(loggerFactory));
}
_logger = loggerFactory.CreateLogger<RootDialogBot>();
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
_botServices = services ?? throw new System.ArgumentNullException(nameof(services));
_studentProfileAccessor = _accessors.UserState.CreateProperty<StudentProfile>("studentProfile");
if (!_botServices.QnAServices.ContainsKey("QnAMaker"))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a QnA service named QnAMaker'.");
}
if (!_botServices.LuisServices.ContainsKey("LUIS"))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a Luis service named LUIS'.");
}
.Add(new Activity2MainDialog(_accessors.UserState, Activity2MainDialog))
.Add(new Activity2LedFailToWorkDialog(_accessors.UserState, Activity2LedFailToWorkDialog));
}
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
...
if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
{
if (turnContext.Activity.MembersAdded != null)
{
// Save the new turn count into the conversation state.
await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
var message = "Welcome!";
await SendWelcomeMessageAsync(turnContext, dc, message,cancellationToken); //Welcome message
}
}
...
}
private static async Task SendWelcomeMessageAsync(ITurnContext turnContext, DialogContext dc,string message, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(message, cancellationToken: cancellationToken);
await dc.BeginDialogAsync(Activity2MainDialog, "activity2MainDialog", cancellationToken);
}
}
}
The dialog (Activity2MainDialog) works fine until it reaches a return await stepContext.ContinueDialogAsync(cancellationToken); call.
Then it halts.
I believe it has something to do with the conversation state but I still couldn't find a solution for that.
Code snippet of the return await stepContext.ContinueDialogAsync(cancellationToken); call
public class Activity2MainDialog : ComponentDialog
{
private static BellaMain BellaMain = new BellaMain();
private static FTDMain FTDMain = new FTDMain();
private readonly IStatePropertyAccessor<StudentProfile> _studentProfileAccessor;
...
public Activity2MainDialog(UserState userState, string dialogMainId)
: base(dialogMainId)
{
InitialDialogId = Id;
_studentProfileAccessor = userState.CreateProperty<StudentProfile>("studentProfile");
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
MsgWelcomeStepAsync
...
};
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
AddDialog(new WaterfallDialog(dialogMainId, waterfallSteps));
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
}
private async Task<DialogTurnResult> MsgWelcomeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync("**Oi**", "Oi", cancellationToken: cancellationToken);
return await stepContext.ContinueDialogAsync(cancellationToken);
}
private async Task<DialogTurnResult> QuestGoAheadStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
message = "Vamos nessa?";
await stepContext.Context.SendActivityAsync(message , message , cancellationToken: cancellationToken);
retryPromptMessage = message;
return await stepContext.PromptAsync(nameof(ChoicePrompt),
new PromptOptions
{
Prompt = null,
RetryPrompt = MessageFactory.Text(retryPromptMessage, retryPromptSpeakMessage), InputHints.ExpectingInput),
Choices = new[]
{
new Choice {Value = "Sim", Synonyms = new List<string> {"yes","yeah","esta","ta","esta","ok","vamos","curti","curtir","claro","tá","sei","top"}},
new Choice {Value = "Não", Synonyms = new List<string> {"no"}}
}.ToList(),
Style = ListStyle.Auto
});
}
Thoughts on how to fix it? Thx
I'm fairly certain the issue is the ContinueDialog call. You need to end that step with:
return await stepContext.NextAsync(null, cancellationToken);
See CoreBot for more example code.
If this doesn't fix your issue, let me know and I'll adjust my answer.

MS Teams: How to initiate a private chat/conversation to a user from a c# backend

I currently try to send a private notification to a user.
What I currently have is a connected bot and connector in my MS Teams environment.
In my c# backend I have the bot which inherit from the ActivityHandler, the tenant id and the user id of ms teams.
The case is now:
Someone is creating an object (for example a Task) in my backend by making an API Post call and I want to notify the user in ms teams.
My idea was now to instantiate the Bot in my API controller (including the ITurnContext). Then with the bot find the right ms teams environment with the tenant id and user id, create a new chat/conversation and send the message. But I guess this is not the right way or I do something wrong. Because I think there is no way to initialise the ITurnContext from my code or?
This is my code and my idea was to use the CreatePrivateConversation method in my API controller.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;
namespace IntegrationsService.Bots
{
public class ProactiveBot : ActivityHandler
{
double _secondsToReply = 3;
ICredentialProvider _credentialProvider;
public ProactiveBot(ICredentialProvider credentialProvider)
{
_credentialProvider = credentialProvider;
}
public async Task<ConversationResourceResponse> CreatePrivateConversation(string message, ITurnContext turnContext)
{
ConnectorClient _client = new ConnectorClient(
new Uri(turnContext.Activity.ServiceUrl),
await GetMicrosoftAppCredentialsAsync(turnContext),
new HttpClient());
var channelData = turnContext.Activity.GetChannelData<TeamsChannelData>();
var conversationParameter = new ConversationParameters
{
Bot = turnContext.Activity.Recipient,
Members = new[] { new ChannelAccount("userid") },
// IsGroup = true,
ChannelData = channelData,
TenantId = channelData.Tenant.Id,
Activity = MessageFactory.Text(message)
};
var response = await _client.Conversations.CreateConversationAsync(conversationParameter);
return response;
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
try
{
await turnContext.SendActivityAsync(MessageFactory.Text($"I'll reply to you in {_secondsToReply} seconds."));
QueueReplyAndSendItProactively(turnContext).Wait();
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
throw e;
}
}
public async Task QueueReplyAndSendItProactively(ITurnContext turnContext)
{
string conversationMessage = "I created my own conversation.";
string replyMessage = "I proactively replied to this conversation.";
var task = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(_secondsToReply));
// Let the Bot Proactively create a a conversation.
var response = await CreateConversation(conversationMessage, turnContext);
// Reply to the conversation which the bot created.
await ProactivelyReplyToConversation(response.Id, replyMessage, turnContext);
return Task.CompletedTask;
});
await task;
}
public async Task<ConversationResourceResponse> CreateConversation(string message, ITurnContext turnContext)
{
ConnectorClient _client = new ConnectorClient(new Uri(turnContext.Activity.ServiceUrl), await GetMicrosoftAppCredentialsAsync(turnContext), new HttpClient());
var channelData = turnContext.Activity.GetChannelData<TeamsChannelData>();
var conversationParameter = new ConversationParameters
{
Bot = turnContext.Activity.Recipient,
IsGroup = true,
ChannelData = channelData,
TenantId = channelData.Tenant.Id,
Activity = MessageFactory.Text(message)
};
var response = await _client.Conversations.CreateConversationAsync(conversationParameter);
return response;
}
public async Task ProactivelyReplyToConversation(string conversationId, string message, ITurnContext turnContext)
{
ConnectorClient _client = new ConnectorClient(new Uri(turnContext.Activity.ServiceUrl), await GetMicrosoftAppCredentialsAsync(turnContext), new HttpClient());
var reply = MessageFactory.Text(message);
reply.Conversation = new ConversationAccount(isGroup: true, id: conversationId);
await _client.Conversations.SendToConversationAsync(reply);
}
private async Task<MicrosoftAppCredentials> GetMicrosoftAppCredentialsAsync(ITurnContext turnContext)
{
ClaimsIdentity claimsIdentity = turnContext.TurnState.Get<ClaimsIdentity>("BotIdentity");
Claim botAppIdClaim = claimsIdentity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AudienceClaim)
??
claimsIdentity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AppIdClaim);
string appPassword = await _credentialProvider.GetAppPasswordAsync(botAppIdClaim.Value).ConfigureAwait(false);
return new MicrosoftAppCredentials(botAppIdClaim.Value, appPassword);
}
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)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"Hello and Welcome!"), cancellationToken);
}
}
}
}
}
Thanks for the help.
There is no way to initialize the ITurnContext from code.
So If you want to send a private proactive message to user, You need to follow below steps
"OnConversationUpdateActivityAsync" method will be called when user install bot. You can get required parameters like ConversationId, ServiceUrl from turnContext.Activity.From object. you can store these details in database when each user installs the bot.
OnConversationUpdateActivityAsync(
ITurnContext turnContext,
CancellationToken cancellationToken)
When user makes an POST API call, you need to pass user id or AadID based on which you can query database and get user details which are needed to send proactive message. Now you can call "CreatePrivateConversation" method and send the proactive message

Welcome message with Ibot interface

I am using the Ibot interface, but the welcome message is sent twice.
I use the OnTurnAsync method, and it seems that doesn't do anything when I call the MembersAdded property.
To send the Welcome message i have this piece of code:
if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate && turnContext.Activity.MembersAdded.Count == 1)
{
userProfile.Welcome = true;
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>();
reply.Attachments.Add(getCard("Welcome"));
await turnContext.SendActivityAsync(reply);
}
There's a few things that could be going on with this.
MembersAdded is called for adding both the bot and the user. I'm guessing this is why it's being sent twice.
If you're using the newer ActivityHandler, you can use:
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)
{
userProfile.Welcome = true;
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>();
reply.Attachments.Add(getCard("Welcome"));
await turnContext.SendActivityAsync(reply);
}
}
}
Or, if you're using the old activity handler:
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (activity.Type == ActivityTypes.ConversationUpdate)
{
if (activity.MembersAdded != null)
{
foreach (var member in activity.MembersAdded)
{
if (member.Name != "Bot" && member.Name != null)
{
userProfile.Welcome = true;
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>();
reply.Attachments.Add(getCard("Welcome"));
await turnContext.SendActivityAsync(reply);
}
}
}
}
[...]
WebChat doesn't automatically send a ConversationUpdate like Emulator does. See this WebChat sample for how to send a welcome event.
You could always ensure it doesn't double-send by checking to see if userProfile.Welcome != true. This sample may help.

Nested dialogs: message being sent twice

I have a bot where I am doing something like this:
1) A new Activity (message) arrives.
2) I dispatch the message to a RootDialog.
3) Depending on some Logic, RootDialog can:
a) Call a LuisDialog (handling natural language)
b) Call a CustomDialog (handles some business logic).
But when the user state is reset, and the flow leads to an intention inside the LuisDialog, it calls the intention method twice. Just the first time the state is empty, then it works fine.
Let me show you the code:
MessagesController:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
try
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
catch (HttpRequestException e)
{
...
}
}
RootDialog:
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> awaitable)
{
bool value = DoSomeCustomLogic();
if (value)
{
string message = DoSomething();
await context.PostAsync(message);
} else {
bool value2 = DoSomeCustomLogic2();
if (value2)
{
var answerValidationDialog = new ValidateAnswerWithUserDialog();
context.Call(answerValidationDialog, ValidateAnswerWithUserDialogCompletedCallback);
} else {
var luisDialog = new LuisDialog();
await context.Forward(luisDialog,LuisDialogCompletedCallback, context.Activity, CancellationToken.None);
}
}
}
Callbacks only do context.Done(true);
And LuisDialog has an Intention which goes like this:
[LuisIntent(LuisUtils.INTENT_MENU_SALUTE)]
public async Task SaluteOrMenu(IDialogContext context, LuisResult result)
{
if (LuisUtils.IntentScoreIsHighEnough(result))
{
string userName = context.Activity.From.Name;
ContextHelper helper = new ContextHelper(MessageReceived);
await helper.AskUserToDoSomeInitialAction(context, saluteWord, userName);
context.Done(true);
}
else
{
await None(context, result);
}
}
And finally class ContextHelper:
public class ContextHelper
{
private Func<IDialogContext, IAwaitable<IMessageActivity>, Task> MessageReceived;
public ContextHelper(Func<IDialogContext, IAwaitable<IMessageActivity>, Task> messageReceived)
{
MessageReceived = messageReceived;
}
public async Task AskUserToDoSomeInitialAction(IDialogContext context, string saluteWord, string userName)
{
var reply = context.MakeMessage();
List<CardAction> buttons = BuildInitialOptionActions();
List<CardImage> images = BuildInitialOptionImages();
string initialText = $"Hi stranger!"
var card = new HeroCard
{
Title = "Hello!"
Text = initialText,
Buttons = buttons,
Images = images
};
reply.Attachments = new List<Attachment> { card.ToAttachment() };
await context.PostAsync(reply);
context.Wait(AfterUserChoseOptionInSalute);
}
private async Task AfterUserChoseOptionInSalute(IDialogContext context, IAwaitable<IMessageActivity> result)
{
await ReDispatchMessageReceivedToDialog(context);
}
private async Task ReDispatchMessageReceivedToDialog(IDialogContext context)
{
await MessageReceived(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
}
The SaluteOrMenu Intention gets called twice (only the first time I interact with the bot or when I delete the state. After Debugging I saw that after doing context.Wait(AfterUserChoseOptionInSalute);, the bot calls that function (instead of waiting for an event to call it)
Any ideas?
Thanks in advance.
I found the line that was wrong. It was on the first dialog (the RootDialog):
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
That is a line that re dispatches a message with the incoming activity. I had it somewhere in some chunk of code and (don't know why), thought it was a good idea to use it on StartAsync. So, because of that, two calls were occurring.
Dumb me.
I just changed it to this and worked:
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}

Resources