I made a bot with bot framework v4, using C#, and it's on a webpage, https://websitebotv2.azurewebsites.net/, if there's only 1 user it works fine but the moment I open it on a new tab it gives a IndexOutOfRangeException when I start the conversation.
What do I need to do to make it work with multiple tabs open?
When my bot stars it creates a waterfall dialog asking the name and greeting the user:
public dialogBotBot(dialogBotAccessors accessors, LuisRecognizer luis, QnAMaker qna)
{
// Set the _accessors
_accessors = accessors ?? throw new ArgumentNullException(nameof(accessors));
// The DialogSet needs a DialogState accessor, it will call it when it has a turn context.
_dialogs = new DialogSet(accessors.ConversationDialogState);
// This array defines how the Waterfall will execute.
var waterfallSteps = new WaterfallStep[] {
NameStepAsync,
NameConfirmStepAsync,
};
// The incoming luis variable is the LUIS Recognizer we added above.
this.Recognizer = luis ?? throw new System.ArgumentNullException(nameof(luis));
// The incoming QnA variable is the QnAMaker we added above.
this.QnA = qna ?? throw new System.ArgumentNullException(nameof(qna));
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
_dialogs.Add(new WaterfallDialog("details", waterfallSteps));
_dialogs.Add(new TextPrompt("name"));
}
Then I will save his name on UserProfile class, which contains the field Name and Context, the Context has the purpose of saving the conversation.
This works the first time, but if I open a new tab or refresh the current tab for a new conversation the bot will fetch the first conversation data.
The Exception is thrown in Startup.cs in:
services.AddBot<dialogBotBot>(options =>
{
options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);
// Catches any errors that occur during a conversation turn and logs them to currently
// configured ILogger.
ILogger logger = _loggerFactory.CreateLogger<dialogBotBot>();
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync(exception + "\nSorry, it looks like something went wrong.\n" + exception.Message);
};
// Create and add conversation state.
var conversationState = new ConversationState(dataStore);
options.State.Add(conversationState);
// Create and add user state.
var userState = new UserState(dataStore);
options.State.Add(userState);
});
My onTurnAsync method is:
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
// Handle Message activity type, which is the main activity type for shown within a conversational interface
// Message activities may contain text, speech, interactive cards, and binary or unknown attachments.
// see https://aka.ms/about-bot-activity-message to learn more about the message and other activity types
if (turnContext.Activity.Type == ActivityTypes.Message)
{
//Get the current user profile
userProfile = await _accessors.UserProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);
userProfile.Contexto.Add(turnContext.Activity.Text);
foreach (string s in userProfile.Contexto)
await turnContext.SendActivityAsync(s);
// Get the conversation state from the turn context.
var state = await _accessors.CounterState.GetAsync(turnContext, () => new CounterState());
// Bump the turn count for this conversation.
state.TurnCount++;
// Check LUIS model
var recognizerResult = await this.Recognizer.RecognizeAsync(turnContext, cancellationToken);
var topIntent = recognizerResult?.GetTopScoringIntent();
// Get the Intent as a string
string strIntent = (topIntent != null) ? topIntent.Value.intent : "";
// Get the IntentScore as a double
double dblIntentScore = (topIntent != null) ? topIntent.Value.score : 0.0;
// Only proceed with LUIS if there is an Intent
// and the score for the Intent is greater than 95
if (strIntent != "" && (dblIntentScore > 2))
{
switch (strIntent)
{
case "None":
//add the bot response to contexto
await turnContext.SendActivityAsync("Desculpa, não percebi.");
break;
case "Utilities_Help":
//add the bot response to contexto
await turnContext.SendActivityAsync("Quero-te ajudar!\nO que precisas?");
break;
default:
// Received an intent we didn't expect, so send its name and score.
//add the bot response to contexto
await turnContext.SendActivityAsync($"Intent: {topIntent.Value.intent} ({topIntent.Value.score}).");
break;
}
}
else
{
if (userProfile.Name == null)
{
// Run the DialogSet - let the framework identify the current state of the dialog from the dialog stack and figure out what (if any) is the active dialog.
var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken);
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
// If the DialogTurnStatus is Empty we should start a new dialog.
if (results.Status == DialogTurnStatus.Empty)
{
await dialogContext.BeginDialogAsync("details", null, cancellationToken);
}
}
else
{
var answers = await this.QnA.GetAnswersAsync(turnContext);
if (answers.Any() && answers[0].Score > 0.7)
{
// If the service produced one or more answers, send the first one.
await turnContext.SendActivityAsync(answers[0].Answer + "\n" + state.TurnCount);
}
else
{
var responseMessage = $"Ainda não sei a resposta mas vou averiguar\nPosso-te ajudar com mais alguma coisa?";
String connectionString = "Data Source=botdataserverv1.database.windows.net;" +
"Initial Catalog=botDataBase;" +
"User id=AzureAdmin#botdataserverv1.database.windows.net;" +
"Password=admin_123;";
SqlConnection connection = new SqlConnection(connectionString);
SqlDataAdapter adapter = new SqlDataAdapter();
SqlCommand command;
String sms = turnContext.Activity.Text;
float result = answers[0].Score;
String insertMessage = "insert into Mensagem(texto,contexto,grauCerteza)" +
"values('" + sms + "', 'Falta apurar o contexto' ," + result + ")";
connection.Open();
command = new SqlCommand(insertMessage, connection);
adapter.InsertCommand = new SqlCommand(insertMessage, connection);
adapter.InsertCommand.ExecuteNonQuery();
command.Dispose();
connection.Close();
await turnContext.SendActivityAsync(responseMessage);
}
}
// Save the user profile updates into the user state.
await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
// Set the property using the accessor.
await _accessors.CounterState.SetAsync(turnContext, state);
// Save the new turn count into the conversation state.
await _accessors.ConversationState.SaveChangesAsync(turnContext);
}
}
}
Your problem is here:
float result = answers[0].Score;
You have:
// Check to see if we have any answers
if (answers.Any() && answers[0].Score > 0.7)
{
[...]
// This is fine
}
else // else, WE HAVE NO ANSWERS
{
[...]
// At this point, answers is an empty array, so answers[0] throws an IndexOutOfRangeException
float result = answers[0].Score;
The reason this happens on refresh is because the new tab's user uses the same User Id. The bot already knows their name, so doesn't show the dialog, and when it calls await this.Recognizer.RecognizeAsync(turnContext, cancellationToken);, the user hasn't entered anything in the new turnContext, so it returns an empty array.
Sidenote: You can set the userID in WebChat with this:
window.WebChat.renderWebChat(
{
directLine: directLine,
userID: "USER_ID" // Make is use Math.random() or something if you want it to be random for each refresh
},
this.botWindowElement.nativeElement
);
Related
We want to receive change notifications from our user's Outlook calendars. We'd like to limit those notifications to only those calendar items that contain our custom property.
CODE WHICH CREATES CALENDAR EVENT
private static async Task<Event> CreateAppointmentAsync(GraphServiceClient graphClient)
{
var newEvent = new Microsoft.Graph.Event
{
Subject = "Test Calendar Appointmnt",
Start = new DateTimeTimeZone() { TimeZone = TimeZoneInfo.Local.Id, DateTime = "2020-11-21T21:00:00" },
End = new DateTimeTimeZone() { TimeZone = TimeZoneInfo.Local.Id, DateTime = "2020-11-21T22:00:00" },
Location = new Location() { DisplayName = "Somewhere" },
Body = new ItemBody { Content = "Some Random Text" },
};
Microsoft.Graph.Event addedEvent;
try
{
newEvent.SingleValueExtendedProperties = new EventSingleValueExtendedPropertiesCollectionPage();
newEvent.SingleValueExtendedProperties.Add(new SingleValueLegacyExtendedProperty { Id = "String {00020329-0000-0000-C000-000000000046} Name CompanyID", Value = "12345" });
addedEvent = await graphClient.Me.Calendar.Events.Request().AddAsync(newEvent);
}
catch (Exception e)
{
throw e;
}
return addedEvent;
}
THE SUBSCRIPTION CODE SNIPPET
var subscription = new Subscription
{
ChangeType = "created",
NotificationUrl ="<OUR-URL>",
Resource = "me/events/?$filter=singleValueExtendedProperties/any(ep: ep/id eq 'String {00020329-0000-0000-C000-000000000046} Name CompanyID' and ep/value ne null)",
ExpirationDateTime = DateTimeOffset.Parse("2020-11-13T18:23:45.9356913Z"),
ClientState = "custom_data_state",
LatestSupportedTlsVersion = "v1_2"
};
The subscription is created successfully, but notifications are not being sent for those items containing the specific custom property described above.
It turns out I was only capturing the "created" notification. I neglected to add "updated" and "deleted".
I was testing with an event that already existed in my calendar. No events were being fired because I didn't create the subscription to detect updates and deletes.
Here is the corrected subscription:
var subscription = new Subscription
{
ChangeType = "created,updated,deleted",
NotificationUrl ="<OUR-URL>",
Resource = "me/events/?$filter=singleValueExtendedProperties/any(ep: ep/id eq 'String {00020329-0000-0000-C000-000000000046} Name CompanyID' and ep/value ne null)",
ExpirationDateTime = DateTimeOffset.Parse("2020-11-13T18:23:45.9356913Z"),
ClientState = "custom_data_state",
LatestSupportedTlsVersion = "v1_2"
};
After 10 to 15 times of return await sc.BeginDialogAsync(nameof(MainDialog));
the error was:
Errors":["The request payload is invalid. Ensure to provide a valid
request payload."]
Scenario:
Use: Enterprise Bot Template
In Main Dialog after Dispatch => QnA Answer Result is asking for feedback using suggestion card [Thumbs Up/Down]
If the user is selecting the Thumbs Up/Down, they are able to end the dialog.
If they do not select any option, the user needs to enter another query that needs to be processed through Maindialog again (DISPATCH => LUIS/QnA).
In my scenario, the user without giving feedback is entering new queries after 10
to 15 times is getting a server error.
var answers = await qnaService.GetAnswersAsync(dc.Context);
if (answers != null && answers.Count() > 0 && answers[0].Score >= _bestQnAScore)
{
string personalizedAnswer = DialogUtilities.PersonalizeAnswer(answers[0].Answer, _authResult, greetingInQNAAnswerSetting);
if (!string.IsNullOrEmpty(personalizedAnswer))
{
personalizedAnswer = StringUtils.URLBlankSpaceHelper(personalizedAnswer);
await dc.Context.SendActivityAsync(personalizedAnswer, personalizedAnswer, "acceptingInput");
string[] arrQnAMetadata = _configuration["QnAMetadata"].ToString().Split(',');
string stringToCheck = (answers[0].Metadata.Length > 0) ? answers[0].Metadata.FirstOrDefault().Value.ToLower() : string.Empty;
bool isChitChat = arrQnAMetadata.Any(stringToCheck.Contains);
if (!isChitChat)
{
await dc.BeginDialogAsync(nameof(FeedbackDialog), new VM_Luis_AuthResult(null, _authResult, _EnumDISPATCHIntent.ToString()));
}
else
{
#region OPEN/CLOSE TICKET
FeedbackLogger.LogUserFeedback(_authResult.UserEmail, dc.Context.Activity.Conversation.Id, dc.Context.Activity.Text, _authResult.Country, _authResult.City, _authResult.OfficeLocation, TicketState.Open, TicketStatus.None, ref this._topScoringIntent, ref this._ticketNumber);
FeedbackLogger.LogUserFeedback(_authResult.UserEmail, dc.Context.Activity.Conversation.Id, dc.Context.Activity.Text, _authResult.Country, _authResult.City, _authResult.OfficeLocation, TicketState.Closed, TicketStatus.Successful, ref this._topScoringIntent, ref this._ticketNumber);
#endregion
await dc.EndDialogAsync();
}
}
}
else
{
await _responder.ReplyWith(dc.Context, MainResponses.ResponseIds.Confused);
await dc.EndDialogAsync();
}
FeedbackDialog.cs
private async Task<DialogTurnResult> OnFeedbackResult(WaterfallStepContext sc, CancellationToken cancellationToken)
{
var res = Convert.ToString(sc.Result);
if (InterruptionsConstants.NegativeFeedback.Any(f => res.ToLower() == f))
{
await sc.Context.SendActivityAsync("Thank You!");
return await sc.EndDialogAsync();
}
else if (InterruptionsConstants.PositiveFeedback.Any(f => res.ToLower() == f))
{
await sc.Context.SendActivityAsync("Thank you very much");
return await sc.EndDialogAsync();
}
else
{
return await sc.BeginDialogAsync(nameof(MainDialog));
}
}
I asked a similar question recently but wasn't specific enough. I see that there is some code with the AdaptiveCards NuGet Package to attach an AdaptiveCardFromJson and AdaptiveCardFromSDK, which under a the normal Microsoft Bot Model is available.
However, under the Microsoft LUIS Bot Model isn't an option, here's the code I have which returns an employee lookup result from a SQL DB Search:
[LuisIntent("Who_is_Employee")]
public async Task Who_is_EmployeeIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation recommendation;
if (result.TryFindEntity("Communication.ContactName", out recommendation))
{
List<Employee> results = EmployeeService.FindEmployees(recommendation.Entity);
if (results.Count > 0)
{
string response = "";
foreach (Employee e in results)
{
string name = e.FullName;
string title = e.JobTitle;
response += " " + name + " " + title + "\n";
}
await context.PostAsync(response);
}
}
else
{
await context.PostAsync(" Sorry, I couldn't find who you were looking for.");
}
}
I would like that information to be returned as an AdaptiveCard, how do I achieve this?
Mark,
you need to craft your adaptive card either as json or using the SDK to create an instance of AdaptiveCard. Here is a great place to learn more about this.
Once you've crafted your card and have an instance of the AdaptiveCard class, you need to create a new message and attach the card to that message. The new message is what you'll post back to the user.
The code will look something like this
var card = AdaptiveCard.FromJson(<your json here>);
Attachment attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card
};
var myRespsonse = context.MakeMessage();
myRespsonse.Attachments.Add(attachment);
await context.PostAsync(myRespsonse, CancellationToken.None);
This was the code I ended up having to use to make this successful:
[LuisIntent("Who_is_Employee")]
public async Task Who_is_EmployeeIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation recommendation;
if (result.TryFindEntity("Communication.ContactName", out recommendation))
{
List<Employee> results = EmployeeService.FindEmployees(recommendation.Entity);
if (results.Count > 0)
{
/* Single line per result */
/*
string response = "";
foreach (Employee e in results)
{
string name = e.FullName;
string title = e.JobTitle;
response += " " + name + " " + title + "\n";
}
await context.PostAsync(response);
*/
/* Adaptive card per result */
// Load json template
string physicalPath = System.Web.HttpContext.Current.Server.MapPath("../AdaptiveCards/EmployeeLookup.json");
string jsonTemplate = "";
using (StreamReader r = new StreamReader(physicalPath))
{
jsonTemplate = r.ReadToEnd();
}
var respsonse = context.MakeMessage();
foreach (Employee e in results)
{
string employeeJson = jsonTemplate;
employeeJson = employeeJson.Replace("{{FullName}}", e.FullName);
employeeJson = employeeJson.Replace("{{JobTitle}}", e.JobTitle);
employeeJson = employeeJson.Replace("{{Reference}}", e.Reference);
employeeJson = employeeJson.Replace("{{Phone}}", e.Phone);
employeeJson = employeeJson.Replace("{{Email}}", e.Email);
employeeJson = employeeJson.Replace("{{Mobile}}", e.Mobile);
AdaptiveCard card = AdaptiveCard.FromJson(employeeJson).Card;
Attachment attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card
};
respsonse.Attachments.Add(attachment);
}
await context.PostAsync(respsonse);
}
}
else
{
await context.PostAsync(" Sorry, I couldn't find who you were looking for.");
}
}
I have enabled signin card in my BOT framework to redirect to the client application URL and it worked perfectly find in the BOT emulator and from the WEBCHAT. But i have issues in the Cortana channel.
While clicking on the button, instead of opening a default browser with the client application ur, a popup opens and says 'we cant connect to the service right now. check your network connection or try again'
Here is my code sample in the BOT framework::
public async Task getmypersonalData(IDialogContext context, LuisResult result)
{
string convid = _convid;
ConnectorClient connector = new ConnectorClient(new Uri(_serviceURL));
var replyToConversation = context.MakeMessage();
// Activity replyToConversation = _activity.CreateReply();
replyToConversation.Recipient = new ChannelAccount(id: _fromAddess);// _activity.From;
replyToConversation.Type = "message";
replyToConversation.Attachments = new List<Attachment>();
List<CardAction> cardButtons = new List<CardAction>();
CardAction plButton = new CardAction()
{
Value = $"{System.Configuration.ConfigurationManager.AppSettings["AppWebSite"]}?conversationid={HttpUtility.UrlEncode(convid)}",
Type = "signin",
Title = "Authentication Required"
};
cardButtons.Add(plButton);
SigninCard plCard = new SigninCard("Please login to the application to access this feature", new List<CardAction>() { plButton });
Attachment plAttachment = plCard.ToAttachment();
replyToConversation.Attachments.Add(plAttachment);
await context.PostAsync(replyToConversation);
}
I have a list of project numbers that I need to process. A project could have about 8000 items and I need to get the data for each item in the project and then push this data into a list of servers. Can anybody please tell me the following..
1) I have 1000 items in iR but only 998 were written to the servers. Did I loose items by using broadCastBlock?
2) Am I doing the await on all actionBlocks correctly?
3) How do I make the database call async?
Here is the database code
public MemcachedDTO GetIR(MemcachedDTO dtoItem)
{
string[] Tables = new string[] { "iowa", "la" };
using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["test"].ConnectionString))
{
using (SqlCommand command = new SqlCommand("test", connection))
{
DataSet Result = new DataSet();
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("#ProjectId", SqlDbType.VarChar);
command.Parameters["#ProjectId"].Value = dtoItem.ProjectId;
connection.Open();
Result.EnforceConstraints = false;
Result.Load(command.ExecuteReader(CommandBehavior.CloseConnection), LoadOption.OverwriteChanges, Tables);
dtoItem.test = Result;
}
}
return dtoItem;
}
Update:
I have updated the code to the below. It just hangs when I run it and only writes 1/4 of the data to the server? Can you please let me know what I am doing wrong?
public static ITargetBlock<T> CreateGuaranteedBroadcastBlock<T>(IEnumerable<ITargetBlock<T>> targets, DataflowBlockOptions options)
{
var targetsList = targets.ToList();
var block = new ActionBlock<T>(
async item =>
{
foreach (var target in targetsList)
{
await target.SendAsync(item);
}
}, new ExecutionDataflowBlockOptions
{
CancellationToken = options.CancellationToken
});
block.Completion.ContinueWith(task =>
{
foreach (var target in targetsList)
{
if (task.Exception != null)
target.Fault(task.Exception);
else
target.Complete();
}
});
return block;
}
[HttpGet]
public async Task< HttpResponseMessage> ReloadItem(string projectQuery)
{
try
{
var linkCompletion = new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = 2
};
var cts = new CancellationTokenSource();
var dbOptions = new DataflowBlockOptions { CancellationToken = cts.Token };
IList<string> projectIds = projectQuery.Split(',').ToList();
IEnumerable<string> serverList = ConfigurationManager.AppSettings["ServerList"].Split(',').Cast<string>();
var iR = new TransformBlock<MemcachedDTO, MemcachedDTO>(
dto => dto.GetIR(dto), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3 });
List<ActionBlock<MemcachedDTO>> actionList = new List<ActionBlock<MemcachedDTO>>();
List<MemcachedDTO> dtoList = new List<MemcachedDTO>();
foreach (string pid in projectIds)
{
IList<MemcachedDTO> dtoTemp = new List<MemcachedDTO>();
dtoTemp = MemcachedDTO.GetItemIdsByProject(pid);
dtoList.AddRange(dtoTemp);
}
foreach (string s in serverList)
{
var action = new ActionBlock<MemcachedDTO>(
async dto => await PostEachServerAsync(dto, s, "setitemcache"));
actionList.Add(action);
}
var bBlock = CreateGuaranteedBroadcastBlock(actionList, dbOptions);
foreach (MemcachedDTO d in dtoList)
{
await iR.SendAsync(d);
}
iR.Complete();
iR.LinkTo(bBlock);
await Task.WhenAll(actionList.Select(action => action.Completion).ToList());
return Request.CreateResponse(HttpStatusCode.OK, new { message = projectIds.ToString() + " reload success" });
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, new { message = ex.Message.ToString() });
}
}
1) I have 1000 items in iR but only 998 were written to the servers. Did I loose items by using broadCastBlock?
Yes in the code below you set BoundedCapacity to one, if at anytime your BroadcastBlock cannot pass along an item it will drop it. Additionally a BroadcastBlock will only propagate Completion to one TargetBlock, do not use PropagateCompletion=true here. If you want all blocks to complete you need to handle Completion manually. This can be done by setting the ContinueWith on the BroadcastBlock to pass Completion to all of the connected targets.
var action = new ActionBlock<MemcachedDTO>(dto => PostEachServerAsync(dto, s, "set"), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3, BoundedCapacity = 1 });
broadcast.LinkTo(action, linkCompletion);
actionList.Add(action);
Option: Instead of the BroadcastBlock use a properly bounded BufferBlock. When your downstream blocks are bound to one item they cannot receive additional items until they finish processing what they have. That will allow the BufferBlock to offer its items to another, possibly idle, ActionBlock.
When you add items into a throttled flow, i.e. a flow with a BoundedCapacity less than Unbounded. You need to be using the SendAsync method or at least handling the return of Post. I'd recommend simply using SendAsync:
foreach (MemcachedDTO d in dtoList)
{
await iR.SendAsync(d);
}
That will force your method signature to become:
public async Task<HttpResponseMessage> ReloadItem(string projectQuery)
2) Am I doing the await on all actionBlocks correctly?
The previous change will permit you to loose the blocking Wait call in favor of a await Task.WhenAlll
iR.Complete();
actionList.ForEach(x => x.Completion.Wait());
To:
iR.Complete();
await bufferBlock.Completion.ContinueWith(tsk => actionList.ForEach(x => x.Complete());
await Task.WhenAll(actionList.Select(action => action.Completion).ToList());
3) How do I make the database call async?
I'm going to leave this open because it should be a separate question unrelated to TPL-Dataflow, but in short use an async Api to access your Db and async will naturally grow through your code base. This should get you started.
BufferBlock vs BroadcastBlock
After re-reading your previous question and the answer from #VMAtm. It seems you want each item sent to All five servers, in that case you will need a BroadcastBlock. You would use a BufferBlock to distribute the messages relatively evenly to a flexible pool of servers that each could handle a message. None the less, you will still need to take control of propagating completion and faults to all the connected ActionBlocks by awaiting the completion of the BroadcastBlock.
To Prevent BroadcastBlock Dropped Messages
In general you two options, set your ActionBlocks to be unbound, which is their default value:
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3, BoundedCapacity = Unbounded });
Or broadcast messages your self from any variety of your own construction. Here is an example implementation from #i3arnon. And another from #svick