Botframework v4 resume conversation outside of the waterfall dialog flow - c#

Botframework V4 webchat
My bot displays a SigninCard to the user, which when clicked user is redirected to an external website where the user will enter login credentials. This external website will perform a service call to a separate endpoint in my bot controller with a token. Now I want to make the bot to display a message to the user, and let the user follow through the rest of the conversation.
For all intents and purposes, it needs to be as same as displaying a proactive message to the user, and jumping to the next waterfall step afterwards.
This is my code so far.
1st approach
Conversation reference information is passed from here.
private async Task<DialogTurnResult> SigninStepStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var convRef = stepContext.Context.Activity.GetConversationReference();
var stateCol = System.Web.HttpUtility.ParseQueryString(string.Empty);
stateCol["userId"] = convRef.User.Id;
stateCol["botId"] = convRef.Bot.Id;
stateCol["conversationId"] = convRef.Conversation.Id;
stateCol["serviceUrl"] = convRef.ServiceUrl;
stateCol["channelId"] = convRef.ChannelId;
SigninCard signinCard = new SigninCard
{
Text = "Please sign-in to continue",
Buttons = new List<CardAction> {
new CardAction(ActionTypes.Signin, "Sign-in",
value: "http://some-external-url?state="+ System.Web.HttpUtility.UrlEncode(stateCol.ToString())) },
};
var reply = stepContext.Context.Activity.CreateReply();
reply.Attachments = new List<Attachment>
{
signinCard.ToAttachment(),
};
return await stepContext.PromptAsync(
"testprompt",
new PromptOptions
{
Prompt = reply,
},
cancellationToken);
}
Conversation reference information is received here. This endpoint is invoked by an external website.
[HttpGet]
public async Task<HttpResponseMessage> Callback(string state)
{
// Resume conversation
var stateCol = System.Web.HttpUtility.ParseQueryString(state);
ConnectorClient connector = new ConnectorClient(new Uri(stateCol["serviceUrl"]));
IMessageActivity newMessage = Activity.CreateMessageActivity();
newMessage.From = new ChannelAccount(stateCol["userId"]);
newMessage.Conversation = new ConversationAccount(id: stateCol["conversationId"]);
newMessage.Recipient = new ChannelAccount(stateCol["botId"]);
newMessage.ChannelId = stateCol["channelId"];
newMessage.Text = "hello user";
newMessage.ReplyToId = stateCol["botId"];
await connector.Conversations.SendToConversationAsync((Activity)newMessage);
//...
}
This piece of code correctly posts "Hello user" message to conversation flow, as a message coming from the bot. I tried connector.Conversations.ReplyTo method, also tried swapping botid and userid values to make it the user replying to the bot. But none of the cases made the waterfall step to jump to the next step in the flow like manually entering a reply text through emulator.
I followed the Idea 2 scenario of this question : https://stackoverflow.com/a/48977832 which is on botfraemwork v3. In botframework v3, it has a resume method as indicated in this answer. But I cannot find this method or anything that makes the conversation to resume, in botframework v4.
Conversation.ResumeAsync(reference, message);
Wondering if there is any special kind of activity that I can send, end the current prompt and make it start the next waterfall step.
2nd approach
On the side, I also tried to follow the ProactiveBot sample code in Botframework 4, to see if that approach will cause the waterfall flow to jump to the next step in the line.
[HttpGet]
public async Task<HttpResponseMessage> Callback(string code, string state, string session_state = null)
{
// Resume conversation
var stateCol = System.Web.HttpUtility.ParseQueryString(state);
MicrosoftAppCredentials.TrustServiceUrl(stateCol["serviceUrl"]);
ConversationReference cr = new ConversationReference();
cr.Bot = new ChannelAccount(stateCol["botId"]);
cr.User = new ChannelAccount(stateCol["userId"]);
cr.ChannelId = stateCol["channelId"];
cr.Conversation = new ConversationAccount(id: stateCol["conversationId"]);
cr.ServiceUrl = stateCol["serviceUrl"];
var msg = cr.GetContinuationActivity();
msg.Text = "login_succeed";
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, cr, BotCallback, default(CancellationToken));
//..
}
But at ContinueConversationAsync it threw below error, even though I am not using any clientSecret parameter anywhere. Not sure if this is because of using the Signin card to invoke the external web site url in the first code block above.
An unhandled exception occurred while processing the request.
ArgumentNullException: Value cannot be null. (Parameter 'clientSecret')
Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential..ctor(string clientId, string clientSecret)

Related

Microsoft Botframework Send Proactive Message to Bot (and not to the User)

we are currently developing some automation with the botframework.
At some point in the conversation, we sent some data through a service bus for processing and wait for a response and then want to continue with the conversation. We already implemented the part where we wait for an response entry in the service bus subscription and then we want to send an Activity from type Event to the bot.
We did the same steps with the proactive message as described in other posts.
We are able to recreate the botclient and conversation reference and all, but in the end when we send the activity, we always send it to the user and not to the bot. But this doesn't trigger the "EventActivityPrompt".
The only way where we achieved the desired outcome was when we made a post to api/messages, but this is too complicated for our taste, and we are looking for an easier way over the botClient (or similar technology)
Has anyone some good ideas? :)
ServiceBusReceiver Message Processing:
private static async Task ProcessMessagesAsync(Message message, CancellationToken token)
{
// Process the message.
Console.WriteLine($"Received message: SequenceNumber:{message.SystemProperties.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}");
_logger?.LogInformation("Received message '{id}' with label '{label}' from queue.", message.MessageId, message.Label);
var data = JsonSerializer.Deserialize<BotCarLicensingOrderRpaRequest>(message.Body);
data.AdditionalData.TryGetValue("ServiceUrl", out var serviceUrl);
data.AdditionalData.TryGetValue("ChannelId", out var channelId);
data.AdditionalData.TryGetValue("BotId", out var botId);
data.AdditionalData.TryGetValue("UserId", out var userId);
data.AdditionalData.TryGetValue("ReplyToId", out var replyToId);
var conversationReference = _offTurnConversationService.CreateSyntheticConversationReference(
channelId?.ToString(),
data.ConversationId,
serviceUrl?.ToString());
conversationReference.User = new ChannelAccount()
{
Id = userId?.ToString(),
Role = "user"
};
conversationReference.Bot = new ChannelAccount
{
Id = botId?.ToString(),
Role = "bot"
};
var activity = (Activity)Activity.CreateEventActivity();
activity.Text = "success";
activity.ChannelId = channelId?.ToString();
activity.ServiceUrl = serviceUrl?.ToString();
activity.RelatesTo = conversationReference;
activity.Conversation = new ConversationAccount
{
Id = data.ConversationId
};
activity.ReplyToId = replyToId?.ToString();
activity.ApplyConversationReference(conversationReference, true);
// Complete the message so that it is not received again.
// This can be done only if the subscriptionClient is created in ReceiveMode.PeekLock mode (which is the default).
await _messageReceiver.CompleteAsync(message.SystemProperties.LockToken);
// This "works" but is complicated, as we have to set up a whole HTTP call
await _offTurnConversationService.SendActivityToBotAsync(activity);
// This just sends the Event to the user, no matter how I set up the conversation
// reference regarding From/Recipient
// And it doesn't help in continuing the conversation
await _offTurnConversationService.SendToConversationThroughPipelineAsync(
async (turnContext, cancellationToken) =>
{
await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken);
},
conversationReference);
// Note: Use the cancellationToken passed as necessary to determine if the subscriptionClient has already been closed.
// If subscriptionClient has already been closed, you can choose to not call CompleteAsync() or AbandonAsync() etc.
// to avoid unnecessary exceptions.
}
OffTurnConversationService:
public ConversationReference CreateSyntheticConversationReference(string channelId, string conversationId, string serviceUrl)
{
ArgumentGuard.NotNull(channelId, nameof(channelId));
ArgumentGuard.NotNull(conversationId, nameof(conversationId));
ArgumentGuard.NotNull(serviceUrl, nameof(serviceUrl));
if (string.IsNullOrEmpty(_botOptions.CurrentValue.BotId))
{
throw new InvalidOperationException("A valid bot id must be configured in your bot options in order to create a synthetic conversation reference.");
}
// WARNING: This implementation works for directline and webchat.
// Changes could be necessary for other channels.
var supportedChannels = new List<string>()
{
Channels.Directline,
Channels.Webchat
};
if (supportedChannels.Any(c => c.Equals(channelId, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogWarning(
"The synthetic conversation reference created for channel {UsedChannel} might not work properly, " +
"because it's not supported and tested. Supported channels are {SupportedChannel}.",
channelId,
string.Join(",", supportedChannels));
}
var conversationReference = new ConversationReference()
{
Conversation = new ConversationAccount()
{
Id = conversationId
},
Bot = new ChannelAccount()
{
Id = _botOptions.CurrentValue.BotId,
Name = _botOptions.CurrentValue.BotId
},
ChannelId = channelId,
ServiceUrl = serviceUrl
};
return conversationReference;
}
public virtual async Task SendActivityToBotAsync(IActivity activity)
{
// Create the new request to POST to the client
var forwardRequest = new HttpRequestMessage()
{
RequestUri = new Uri(_botOptions.CurrentValue.ReplyServiceUrl),
Method = HttpMethod.Post,
};
// Change the host for the request to be the forwarding URL.
forwardRequest.Headers.Host = forwardRequest.RequestUri.Host;
// If the child bot is not running on local mode (no app-id/password),
// we're going send an authentication header.
OAuthResponse authToken = await GetTokenAsync(_botOptions.CurrentValue.MicrosoftAppId, _botOptions.CurrentValue.MicrosoftAppPassword);
forwardRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken.AccessToken);
// Altered activity to JSON content
var json = JsonConvert.SerializeObject(activity);
var content = new StringContent(json, Encoding.UTF8, "application/json");
forwardRequest.Content = content;
using var client = new HttpClient();
var response = await client.SendAsync(forwardRequest);
if (!response.IsSuccessStatusCode)
{
string message = $"Failed to send activity '{activity.Id}' to client bot. {response.ReasonPhrase}";
throw new Exception(message);
}
}
public virtual async Task SendToConversationThroughPipelineAsync(
BotCallbackHandler callback,
ConversationReference conversationReference)
{
ArgumentGuard.NotNull(callback, nameof(callback));
ArgumentGuard.NotNull(conversationReference, nameof(conversationReference));
// Avoiding 401 "Unauthorized" errors
TrustServiceUrl(conversationReference.ServiceUrl);
// Reuse adapter with its pipeline to send responses back to the user (like pro-active messages)
await ((BotAdapter)_botFrameworkHttpAdapter).ContinueConversationAsync(
_botOptions.CurrentValue.MicrosoftAppId,
conversationReference,
callback,
default);
}

How can I proactively trigger a dialog from a bot without a turnContext?

I have a Teams bot that can send proactive messages to a user via webAPI. I can get a ConnectorClient from the Microsoft.Bot.Connector namespace, and then from there I can identify the relevant conversation, and call SendToConversationAsync to message a user.
If I want to use this to initiate a dialog though, the challenge seems to be that I don't have a TurnContext to reference. I found a post here that seemed promising, but that still depends on having a dialog turn running. Ideally, I'd like to be able to do this via the ConnectorClient reference that I already have, and trying with a null TurnContext doesn't seem to work. For example, trying this:
var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
var dialogSet = new DialogSet(dialogState);
dialogSet.Add(new MyDialog());
DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var turnResult = await dc.BeginDialogAsync("MyDialog");
Throws an exception if turnContext is null.
This answer here has some promising ideas too, suggesting faking an incoming event, but I can do something like CreateInvokeActivity(), but sending that to the conversation throws an exception. I'm also not sure how to trigger the pipeline to get the message through in the same process without going as far up as using an HTTPCLient to POST the raw message (which requires getting a token I believe). The bot already has a 1:1 conversation with the user, but I'd like to have this initiate a dialog if possible. Is there a way to have the ConnectorClient begin a dialog proactively, or trigger an invoke to the bot pipeline programmatically to allow it to kick off there?
I managed to figure out a way to do this, but it's probably not an ideal scenario. I wanted to start a dialog from the API, specifically an authentication dialog that gets a user's OAuth token for accessing graph. If the user is signed in, the token is returned immediately, and if not, they get a sign in prompt. I have something like this in my bot code (edited for brevity):
public static async Task<string> GetTokenAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
var dialogSet = new DialogSet(dialogState);
dialogSet.Add(new AuthDialog());
DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var turnResult = await dc.BeginDialogAsync("AuthDialog");
await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
if(turnResult.Status== DialogTurnStatus.Waiting)
{
_log.Debug("Got login request for user-waiting for response");
return string.Empty;
}
else if(turnResult.Result is TokenResponse)
{
return ((TokenResponse)turnResult.Result).Token;
}
return null;
}
This creates the dialog and, if possible, returns the token. In my webAPI, I have something like this to invoke it proactively:
string conversationID = "CONV_ID_FROM_STATE";
var members = await m_client.Conversations.GetConversationMembersAsync(conversationID);
BotFrameworkAdapter b = new BotFrameworkAdapter(new SimpleCredentialProvider("BOT ID", "BOT_SECRET"));
var message = Activity.CreateMessageActivity();
message.Text = "login";
message.From = new ChannelAccount(members[0].Id);
message.Conversation = new ConversationAccount(id: conversationID, conversationType: "personal", tenantId: :BOT_TENANT_ID);
message.ChannelId = "msteams";
TurnContext t = new TurnContext(b, (Activity)message);
ClaimsIdentity id = new ClaimsIdentity();
id.AddClaim(new Claim("aud", "BOT_ID"));
t.TurnState.Add("BotIdentity", id);
t.TurnState.Add("Microsoft.Bot.Builder.BotAdapter.OAuthScope", "https://api.botframework.com");
t.TurnState.Add("Microsoft.Bot.Connector.IConnectorClient", m_client);
string token = await myBot<AuthDialog>.GetTokenAsync(t, default);
At this point, if the token is an empty string, the user hasn't signed in, but otherwise it should be a valid token to make graph calls with. I've tested this with a few new accounts, and it seems to work, so I'm calling that a win for now. If there's something that's fundamentally busted here though, please comment.
You can create a turn context from a conversion reference using ContinueConversationAsync. Please refer to the docs and the sample for more information.

Send sms notify message to user when his/her account locked, how can i do that in C#

Bot Info
SDK : C#
Active Channels: SMS (Twilio)
Bot version: v4.4.3
Issue Description:
I'd like to be able to send proactive messages through SMS messages. When a user's account locked, i have that person's phone number, and i want to send a notify message like "your account is locked, please do something." Is this possible?
i checked the documentation about proactive message, which is get the "ConversationReference" through "activity", i don't know with the phone number, can i create a "ConversationReference" object, and how to tell bot about the phone number through notify controller.
Thank you.
Twilio developer evangelist here.
If you don't already have a conversation reference from a previous conversation then the documentation doesn't seem clear on how you would start a conversation. In this case, it might be easier to send the user the SMS message using the Twilio API directly.
Fortunately, unlike most channels, you can construct a conversation reference without having to have the user message the bot first since you know the user's number and you have the bot's number. Take a look at the code snippet below. You can send a proactive message to a phone number by sending a get request to http://localhost:3978/api/notify/+1##########
using Microsoft.Bot.Connector.Authentication;
[HttpGet("{number}")]
public async Task<IActionResult> Get(string number)
{
MicrosoftAppCredentials.TrustServiceUrl("https://sms.botframework.com/");
var conversationReference = new ConversationReference {
User = new ChannelAccount { Id = number },
Bot = new ChannelAccount { Id = "<BOT_NUMBER>" },
Conversation = new ConversationAccount { Id = number },
ServiceUrl = "https://sms.botframework.com/"
};
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, BotCallback, default(CancellationToken));
// Let the caller know proactive messages have been sent
return new ContentResult()
{
Content = "<html><body><h1>Proactive messages have been sent.</h1></body></html>",
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
};
}
private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync("proactive hello");
}
For more details on sending proactive messages, take a look at the Proactive Message sample.
Hope this helps.

Bot Framework Cortana C# Adaptive Cards Action Buttons no longer working

Has something changed with Cortana in the last few days?
I have buttons on an adaptive card which now do nothing. I have remote debugged and hit all breakpoints as expected but when tapping on a button nothing happens. It is as though the buttons are disabled somehow.
Everything works fine in the emulator.
My Bot code simply shows buttons in an adaptive card which then post their DataJson value which is received by the MessageReceivedAsync method.
I have been refining how this all works as I found that Cortana has a limit of 5 actions on one card. I did a bit of a work around in this and make each button appear on a card.
Therefore, I have been thinking that in my refining, I have done something to make these buttons no longer work.
However, I have now put my code back to how it was days ago which definitely did work in Cortana and it now does not work.
My question therefore is, has anything changed in the Cortana side to stop this?
Thanks!
I modified the echodemo to put up a simple adaptive card and it worked as documented: "Thanks for Clicking! SomeType is SomeData". (I am logging a ticket to update documentation to v1.0.3 of adaptive cards).
public async Task ClickHandleAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
try
{
string someValue = "unknown";
if (message.Value != null)
{
// Got an Action Submit
dynamic value = message.Value;
string s = value.ToString();
Trace.WriteLine(s);
someValue = value.SomeType;
}
else
Trace.TraceInformation("There is no value");
//string data = message.ChannelData.ToString();
//Trace.WriteLine(data);
//Trace.TraceInformation("stringify message");
//string json = new JavaScriptSerializer().Serialize(message);
//Trace.WriteLine(json);
await context.PostAsync("Thanks for the click! SomeType is " + someValue);
context.Wait(MessageReceivedAsync);
}
catch (Exception e)
{
Trace.TraceInformation(e.ToString());
}
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (message.Text == "show card")
{
var response = context.MakeMessage();
if (response.Attachments == null)
response.Attachments = new List<Attachment>();
AdaptiveCard card = new AdaptiveCard();
card.Body.Add(new AdaptiveTextBlock()
{
Text = "This is a test",
Weight = AdaptiveTextWeight.Bolder,
Size = AdaptiveTextSize.Medium
});
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = "Click Me",
Id = "12345678",
DataJson = "{ \"SomeType\": \"SomeData\" }"
});
Attachment attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card,
Name = "MyCard"
};
response.Attachments.Add(attachment);
await context.PostAsync(response);
context.Wait(ClickHandleAsync);
}

Microsoft DirectLine API to bot does not work

So to explain my problem, I have to give you the context.
I got a Bot built with microsoft bot framework deployed on slack. Now it can happen these "events" on my backend that the bot communicates with. When a event occurs, I want to notify my bot of it and then let it send a message to all of it's conversations that something has happend. So basicly:
Backend>Microserivce>Bot>users
To do this I have to store all conversations in my backend, which I do in a database there. When a event happends, the backend will post an activity to the bot with all the conversations(basicly their id's) and the event it should show them.
So in essence my backend need to post a message to my bot.
For doing this I found the microsoft directline api which acts as a middleman here, a more abstract way to talk with the bot. The problem is that I don't know how to do it. I followed microsofts own tutorial but it doesn't seem to work for me:
This is the endpoint that my backend uses to notify the bot. "content" contains conversations and events as a json formated string.
[HttpPost]
[Route("conversationsEvents")]
public HttpResponseMessage PostConversationsEvents([FromBody]string content)
{
NotifyBot.Notify(content);
return Request.CreateResponse(HttpStatusCode.NoContent );
}
NotifyBot.Notify(content) looks like this:
private static async Task StartBotConversation( string contents)
{
string directLineSecret = "secret";
string fromUser = "microserviceNotifyEndpoint";
Activity activity = new Activity
{
From = new ChannelAccount(fromUser),
Text = contents,
Type = ActivityTypes.Event
};
DirectLineClient client = new DirectLineClient(directLineSecret);
var conversation = await client.Conversations.StartConversationAsync();
await client.Conversations.PostActivityAsync(conversation.ConversationId, activity);
}
Basicly the execution get's stuck at var conversation = await client.Conversations.StartConversationAsync(); , it just waits forever.
I tried changing it to var conversation = await client.Conversations.StartConversationAsync().ConfigureAwait(continueOnCapturedContext: false);´the execution goes on but the activity doesn't seem to get posted.
I'm not sure why the call to .StartConversationAsync() would freeze in your case. Maybe you haven't enabled the Direct Line channel on dev.botframework.com/bots? Nonetheless, as pointed out by Sergey, the Direct Line is a Channel and not a means for communicating with your bot on other channels.
Check out the Connector Client: bot-builder-dotnet-connector
Here is a static example of using it to proactively send a message to a user from a bot: MicrosoftDX/botFramework-proactiveMessages - sample: ConversationStarter.cs
pertinent code from sample:
public static async Task Resume(string conversationId,string channelId)
{
var userAccount = new ChannelAccount(toId,toName);
var botAccount = new ChannelAccount(fromId, fromName);
var connector = new ConnectorClient(new Uri(serviceUrl));
IMessageActivity message = Activity.CreateMessageActivity();
if (!string.IsNullOrEmpty(conversationId) && !string.IsNullOrEmpty(channelId))
{
message.ChannelId = channelId;
}
else
{
conversationId = (await connector.Conversations.CreateDirectConversationAsync( botAccount, userAccount)).Id;
}
message.From = botAccount;
message.Recipient = userAccount;
message.Conversation = new ConversationAccount(id: conversationId);
message.Text = "Hello, this is a notification";
message.Locale = "en-Us";
await connector.Conversations.SendToConversationAsync((Activity)message);
}
The serviceUrl, the channelId, conversationId, toId, fromId, etc are cached from previous communication by the user to the bot (these are statically stored in this example, so only work for one user). This example shows how it is possible to proactively send a message to a user from a bot. The Direct Line api is not required.
You don't need to use DirectLine, it is designed for creating alternative bot UIs.
To implementing what your want, you may try the following:
First, you need to store users addresses to whom you want to send the messages. It my be done by storing the ResumptionCookie of a user last message in your backend database.
var state = new ResumptionCookie(message).GZipSerialize();
When your PostConversationsEvents is called, you may resume the conversation at the latest point with each users.
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = resumptionCookie.GetMessage();
message.Text = content;
await Conversation.ResumeAsync(resumptionCookie, message);
It is not the only solution. As I said, in this case you just resumed the conversation with the user at the latest point. Another solution is to save the user address (user the same ResumptionCookie class) but start the conversation when you need to:
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = cookie.GetMessage();
ConnectorClient client = new ConnectorClient(new Uri(message.ServiceUrl));
var conversation = await
client.Conversations.CreateDirectConversationAsync(message.Recipient, message.From);
message.Conversation.Id = conversation.Id;
var newMessage = message.CreateReply();
newMessage.Text = content;
await client.Conversations.SendToConversationAsync(newMessage);
See more details on BotFramework documentation.

Categories