When running a multi-turn conversation I am getting different results between GetAnswersRawAsync and GetAnswersAsync. GetAnswersRawAsync returns zero results about every 5th call where as GetAnswersAsync seems to always return. Is this a bug? What is the difference between the two? The documentation for both seems the same.
private async Task<DialogTurnResult> CallGenerateAnswerAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var qnaMakerOptions = new QnAMakerOptions
{
ScoreThreshold = DefaultThreshold,
Top = DefaultTopN,
Context = new QnARequestContext(),
QnAId = 0
};
var dialogOptions = GetDialogOptionsValue(stepContext);
// Getting options
if (dialogOptions.ContainsKey(QnAOptions))
{
qnaMakerOptions = dialogOptions[QnAOptions] as QnAMakerOptions;
qnaMakerOptions.ScoreThreshold = qnaMakerOptions?.ScoreThreshold ?? DefaultThreshold;
qnaMakerOptions.Top = DefaultTopN;
}
// Storing the context info
stepContext.Values[CurrentQuery] = stepContext.Context.Activity.Text;
// -Check if previous context is present, if yes then put it with the query
// -Check for id if query is present in reverse index.
if (!dialogOptions.ContainsKey(QnAContextData))
{
dialogOptions[QnAContextData] = new Dictionary<string, int>();
}
else
{
var previousContextData = dialogOptions[QnAContextData] as Dictionary<string, int>;
if (dialogOptions[PreviousQnAId] != null)
{
var previousQnAId = Convert.ToInt32(dialogOptions[PreviousQnAId]);
if (previousQnAId > 0)
{
qnaMakerOptions.Context = new QnARequestContext
{
PreviousQnAId = previousQnAId
};
qnaMakerOptions.QnAId = 0;
if (previousContextData.TryGetValue(stepContext.Context.Activity.Text.ToLower(), out var currentQnAId))
{
qnaMakerOptions.QnAId = currentQnAId;
}
}
}
}
var svc = _services.QnAMakerService;
// Does not return a result
var response = await svc.GetAnswersRawAsync(stepContext.Context, qnaMakerOptions).ConfigureAwait(false);
// Returns a result
var response2 = await svc.GetAnswersAsync(stepContext.Context, qnaMakerOptions).ConfigureAwait(false);
// Additional code removed for simplification
}
The difference between the 2 is that one returns the entire QnA Maker result whereas the other only returns the Answers property of the whole result.
GetAnswersAsync() - returns QueryResult.Answer
GetAnswersAsync(TurnContext, QnAMakerOptions) eventually calls the following GetAnswersAsync overload
public async Task<QueryResult[]> GetAnswersAsync(
ITurnContext turnContext,
QnAMakerOptions options,
Dictionary<string, string> telemetryProperties,
Dictionary<string, double> telemetryMetrics = null)
{
var result = await GetAnswersRawAsync(turnContext, options, telemetryProperties, telemetryMetrics).ConfigureAwait(false);
return result.Answers;
}
Notice how it just calls GetAnswersRawAsync and from those results, it only returns its answer.
vs.
GetAnswersRawAsync() - returns entire QueryResult
public async Task<QueryResults> GetAnswersRawAsync(
ITurnContext turnContext,
QnAMakerOptions options,
Dictionary<string, string> telemetryProperties = null,
Dictionary<string, double> telemetryMetrics = null)
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnContext.Activity == null)
{
throw new ArgumentException($"The {nameof(turnContext.Activity)} property for {nameof(turnContext)} can't be null.", nameof(turnContext));
}
var messageActivity = turnContext.Activity.AsMessageActivity();
if (messageActivity == null)
{
throw new ArgumentException("Activity type is not a message");
}
if (string.IsNullOrEmpty(turnContext.Activity.Text))
{
throw new ArgumentException("Null or empty text");
}
var result = await this._generateAnswerHelper.GetAnswersRawAsync(turnContext, messageActivity, options).ConfigureAwait(false);
await OnQnaResultsAsync(result.Answers, turnContext, telemetryProperties, telemetryMetrics, CancellationToken.None).ConfigureAwait(false);
return result;
}
You can see the source code in the botbuidler-dotnet repo here.
Related
How to run an async Task without blocking other tasks?
I have one function that iterates though a List but the problem is that when the function is called other functions won't work again until the first function is done. What are the ways of making the HandleAsync function non-blocking ?
public static async Task HandleAsync(Message message, TelegramBotClient bot)
{
await Search(message, bot); // This should be handled without working other possible functions. I have a function similar to this but which doesn't iterate though any list.
}
private static async Task Search(Message message, TelegramBotClient bot)
{
var textSplit = message.Text.Split(new[] {' '}, 2);
if (textSplit.Length == 1)
{
await bot.SendTextMessageAsync(message.From.Id, "Failed to fetch sales. Missing game name. ",
ParseMode.Html);
}
else
{
var search = await Program.itad.SearchGameAsync(textSplit[1], limit: 10, cts: Program.Cts);
if (search.Data != null)
{
var builder = new StringBuilder();
foreach (var deal in search.Data.List)
{
var title = deal.Title;
var plain = deal.Plain;
var shop = deal.Shop != null ? deal.Shop.Name : "N/A";
var urls = deal.Urls;
var priceNew = deal.PriceNew;
var priceOld = deal.PriceOld;
var priceCut = deal.PriceCut;
builder.AppendLine($"<b>Title:</b> {title}");
builder.AppendLine($"<b>Shop:</b> {shop}");
builder.AppendLine();
builder.AppendLine($"<b>Price:</b> <strike>{priceOld}β¬</strike> | {priceNew}β¬ (-{priceCut}%)");
var buttons = new[]
{
new[]
{
InlineKeyboardButton.WithUrl("Buy", urls.Buy.AbsoluteUri),
InlineKeyboardButton.WithUrl("History",
urls.Game.AbsoluteUri.Replace("info", "history"))
}
};
var keyboard = new InlineKeyboardMarkup(buttons);
var info = await Program.itad.GetInfoAsync(plain, cts: Program.Cts);
var image = info.Data.GameInfo.Image;
if (image == null) image = new Uri("https://i.imgur.com/J7zLBLg.png");
await TelegramBot.Bot.SendPhotoAsync(message.From.Id, new InputOnlineFile(image.AbsoluteUri),
builder.ToString(), ParseMode.Html, replyMarkup: keyboard,
cancellationToken: Program.Cts.Token);
builder.Clear();
}
}
else
{
await bot.SendTextMessageAsync(message.From.Id, "Failed to fetch sales. Game not found. ",
ParseMode.Html);
}
}
A bot doesn't go to origin!=null. I have tried taking origin as a separate step, still the same bug occurs.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Logging;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;
using Newtonsoft.Json;
namespace Microsoft.BotBuilderSamples.Dialogs
{
public class MainDialog : ComponentDialog
{
private readonly FlightBookingRecognizer _luisRecognizer;
protected readonly ILogger Logger;
// Dependency injection uses this constructor to instantiate MainDialog
public MainDialog(FlightBookingRecognizer luisRecognizer, BookingDialog bookingDialog, ILogger<MainDialog> logger)
: base(nameof(MainDialog))
{
_luisRecognizer = luisRecognizer;
Logger = logger;
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(bookingDialog);
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
IntroStepAsync,
ActStepAsync,
OriginStepAsync,
OriginConfirmAsync,
FinalStepAsync,
}));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private const string DestinationStepMsgText = "Where would you like to travel to?";
private const string OriginStepMsgText = "Where are you traveling from?";
private async Task<DialogTurnResult> IntroStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if (!_luisRecognizer.IsConfigured)
{
await stepContext.Context.SendActivityAsync(
MessageFactory.Text("NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' to the appsettings.json file.", inputHint: InputHints.IgnoringInput), cancellationToken);
return await stepContext.NextAsync(null, cancellationToken);
}
// Use the text provided in FinalStepAsync or the default if it is the first time.
var messageText = stepContext.Options?.ToString() ?? "What can I help you with today?\nSay something like \"Book a flight from Paris to Berlin on March 22, 2020\"";
var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput);
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
}
private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if (!_luisRecognizer.IsConfigured)
{
// LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken);
}
// Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);
switch (luisResult.TopIntent().intent)
{
//case FlightBooking.Intent.BookFlight:
// await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);
// // Initialize BookingDetails with any entities we may have found in the response.
// //var bookingDetails = new BookingDetails()
// //{
// // // Get destination and origin from the composite entities arrays.
// // Destination = luisResult.ToEntities.Airport,
// // Origin = luisResult.FromEntities.Airport,
// // TravelDate = luisResult.TravelDate,
// //};
// return await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken);
case FlightBooking.Intent.TravellerDeatail:
//await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);
// Initialize BookingDetails with any entities we may have found in the response.
var bookingDetails = new BookingDetails()
{
// Get destination and origin from the composite entities arrays.
Destination = luisResult.DestinationEntities.Destination,
Origin = luisResult.OriginEntities.Origin,
TravelDate = luisResult.TravelDate,
};
if (bookingDetails.Destination == null)
{
var messageText1 = stepContext.Options?.ToString() ?? DestinationStepMsgText;
var promptMessage1 = MessageFactory.Text(messageText1, messageText1, InputHints.ExpectingInput);
await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage1 }, cancellationToken);
var promptMessage3 = "";
return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage3, cancellationToken);
}
if (bookingDetails.Destination != null)
{
string[] SplitDest;
string combined = "";
string collection = "";
string PreDest = bookingDetails.Destination;
string ConDest = bookingDetails.ConDestination;
try
{
if (PreDest.Length != 3)
{
var keyword = PreDest;
string[] strCity = File.ReadAllText(#"D:\Ravvise project\Attarbot2-src\AdditionalIN\Airports.txt").Split('$');
foreach (string str in strCity)
{
if (str.IndexOf(keyword, StringComparison.CurrentCultureIgnoreCase) >= 0)
{
combined += str + '$';
}
}
}
SplitDest = combined.Split("$");
foreach (var prepoll in SplitDest)
{
if (SplitDest.Length == 1)
{
ConDest = SplitDest[0];
ConDest = SplitDest[0].Split("[")[1].Replace("]", "");
}
else if (SplitDest.Length > 1)
{
string DestBody = File.ReadAllText(#"D:\Ravvise project\Attarbot2-src\AdditionalIN\DestBody.txt");
string strTemp = "";
collection = "";
foreach (string city in SplitDest)
{
if (city != "")
{
ConDest = city.Split("[")[1].Replace("]", "");
strTemp = DestBody.Replace("{ZZZ}", city);
strTemp = strTemp.Replace("{YYY}", ConDest);
collection += strTemp;
}
}
collection = collection.TrimEnd(',');
string DestTemplate = File.ReadAllText(#"D:\Ravvise project\Attarbot2-src\AdditionalIN\DestTemplate.txt");
DestTemplate = DestTemplate.Replace("{XXX}", collection);
var cardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(DestTemplate.ToString()),
};
var opts = new PromptOptions
{
Prompt = new Activity
{
Attachments = new List<Attachment>() { cardAttachment },
Type = ActivityTypes.Message,
Text = "Kindly select any Combinations from below.", // You can comment this out if you don't want to display any text. Still works.
}
};
// Display a Text Prompt and wait for input
return await stepContext.PromptAsync(nameof(TextPrompt), opts);
}
}
}
catch (Exception ex) { }
return await stepContext.ContinueDialogAsync();
}
if (bookingDetails.Origin == null)
{
var messageText1 = stepContext.Options?.ToString() ?? "Wow ππππππππ" + bookingDetails.Destination + " is a great place to explore.Kindly tell your Origin.";
var promptMessage1 = MessageFactory.Text(messageText1, messageText1, InputHints.ExpectingInput);
await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage1 }, cancellationToken);
var promptMessage3 = "";
return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage3, cancellationToken);
}
var promptMessage5 = "";
return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage5, cancellationToken);
case FlightBooking.Intent.Greeting:
var messageText = stepContext.Options?.ToString() ?? "Gretings Mate!!!!!!πππI wink cus I wanna help you with yourπππππSo.....Kindly tell me your travel deatails.";
var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput);
await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
var promptMessage2 = "";
return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage2, cancellationToken);
// Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder.
case FlightBooking.Intent.GetWeather:
// We haven't implemented the GetWeatherDialog so we just display a TODO message.
var getWeatherMessageText = "TODO: get weather flow here";
var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
break;
default:
// Catch all for unhandled intents
var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
break;
}
return await stepContext.NextAsync(null, cancellationToken);
}
// Shows a warning if the requested From or To cities are recognized as entities but they are not in the Airport entity list.
// In some cases LUIS will recognize the From and To composite entities as a valid cities but the From and To Airport values
// will be empty if those entity values can't be mapped to a canonical item in the Airport.
private async Task<DialogTurnResult> OriginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
bookingDetails.ConDestination = (string)stepContext.Result;
if (bookingDetails.Origin == null)
{
var messageText1 = stepContext.Options?.ToString() ?? "Wow ππππππππ" + bookingDetails.Destination + " is a great place to explore.Kindly tell your Origin.";
var promptMessage1 = MessageFactory.Text(messageText1, messageText1, InputHints.ExpectingInput);
await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage1 }, cancellationToken);
}
var promptMessage3 = "";
return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage3, cancellationToken);
}
private async Task<DialogTurnResult> OriginConfirmAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
string[] SplitOrigin;
string combined = "";
string collection = "";
string PConOrigin = "";
var bookingDetails = (BookingDetails)stepContext.Options;
bookingDetails.Origin = (string)stepContext.Result;
string PreOrigin = bookingDetails.Origin;
string ConOrigin = bookingDetails.ConOrigin;
try
{
if (PreOrigin.Length != 3)
{
var keyword = PreOrigin;
string[] strCity = File.ReadAllText("E:/Airports.txt").Split('$');
foreach (string str in strCity)
{
if (str.IndexOf(keyword, StringComparison.CurrentCultureIgnoreCase) >= 0)
{
combined += str + '$';
}
}
}
SplitOrigin = combined.Split("$");
foreach (var prepoll in SplitOrigin)
{
if (SplitOrigin.Length == 1)
{
ConOrigin = SplitOrigin[0];
ConOrigin = SplitOrigin[0].Split("[")[1].Replace("]", "");
}
else if (SplitOrigin.Length > 1)
{
string DestBody = File.ReadAllText("E:/DestBody.txt");
string strTemp = "";
collection = "";
foreach (string city in SplitOrigin)
{
if (city != "")
{
ConOrigin = city.Split("[")[1].Replace("]", "");
strTemp = DestBody.Replace("{ZZZ}", city);
strTemp = strTemp.Replace("{YYY}", ConOrigin);
collection += strTemp;
}
}
collection = collection.TrimEnd(',');
string DestTemplate = File.ReadAllText("E:/DestTemplate.txt");
DestTemplate = DestTemplate.Replace("{XXX}", collection);
var cardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(DestTemplate.ToString()),
};
var opts = new PromptOptions
{
Prompt = new Activity
{
Attachments = new List<Attachment>() { cardAttachment },
Type = ActivityTypes.Message,
Text = "Kindly select any Combinations from below.", // You can comment this out if you don't want to display any text. Still works.
}
};
// Display a Text Prompt and wait for input
return await stepContext.PromptAsync(nameof(TextPrompt), opts);
}
}
}
catch (Exception ex) { }
return await stepContext.NextAsync(bookingDetails.ConOrigin, cancellationToken);
}
private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// If the child dialog ("BookingDialog") was cancelled, the user failed to confirm or if the intent wasn't BookFlight
// the Result here will be null.
if (stepContext.Result is BookingDetails result)
{
// Now we have all the booking details call the booking service.
// If the call to the booking service was successful tell the user.
var timeProperty = new TimexProperty(result.TravelDate);
var travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
var messageText = $"I have you booked to {result.Destination} from {result.Origin} on {travelDateMsg}";
var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
await stepContext.Context.SendActivityAsync(message, cancellationToken);
}
// Restart the main dialog with a different message the second time around
var promptMessage = "What else can I do for you?";
return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage, cancellationToken);
}
}
}
After destination != null, bot doesn't go to bookingdetails.origin= null. If it is not possible please guide any other way to do it like different steps to get different values. I tried to do it but still, the same problem occurs.
I need to add a question 'Did this help?' after getting the response from QnA and take the feedback from user. If there is no response for this and if the next input is a completely new query, the flow should restart from bot.cs
I tried using a textprompt, but when tested in emulator, bot doesn't wait for user input after the prompt.
Bot.cs
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
var activity = turnContext.Activity;
var dc = await _dialogs.CreateContextAsync(turnContext);
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnContext.Activity.Type == ActivityTypes.Message)
{
if (turnContext.Activity.Text != null)
{
var luisResults = await _services.LuisServices[LuisConfiguration].RecognizeAsync(dc.Context, cancellationToken);
var luisProperties = LuisEntities.FromLuisResults(luisResults);
await _luisEntitiesAccessor.SetAsync(turnContext, luisProperties);
var topScoringIntent = luisResults?.GetTopScoringIntent();
var topIntent = topScoringIntent.Value.intent;
switch (topIntent)
{
case NoneIntent:
await dc.BeginDialogAsync(QnADialog.Name);
break;
case GreetingsIntent:
await dc.BeginDialogAsync(QnAGreetingsDialog.Name);
break;
case CredentialsIntent:
await dc.BeginDialogAsync(CredentialsDialog.Name);
break;
case ContactusIntent:
await dc.BeginDialogAsync(FeedbackDialog.Name);
break;
case FeedbackIntent:
await dc.BeginDialogAsync(FeedbackDialog.Name);
break;
default:
await dc.Context.SendActivityAsync("I didn't understand what you just said to me.");
break;
}
}
else if (string.IsNullOrEmpty(turnContext.Activity.Text))
{
await HandleSubmitActionAsync(turnContext, userProfile);
}
}
else if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
{
if (turnContext.Activity.MembersAdded != null)
{
await SendWelcomeMessageAsync(turnContext);
}
}
else if (turnContext.Activity.Type == ActivityTypes.Event)
{
await SendWelcomeMessageAsync(turnContext);
}
else
{
await turnContext.SendActivityAsync($"{turnContext.Activity.Type} event detected");
}
// Save the dialog state into the conversation state.
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
QnADialog.cs - dialog in which I want the prompt to work
public class QnADialog : ComponentDialog
{
public const int QnaNumResults = 1;
public const double QnaConfidenceThreshold = 0.5;
public const string QnaConfiguration = "QnAFaqSubscriptionKey";
private const string QnAFeedbackDialog = "qnaDialog";
public const string Name = "QnA";
public const string TextPrompt = "textPrompt";
private readonly BotServices _services;
private readonly IStatePropertyAccessor<UserProfile> _userProfileAccessor;
Action<string, string, bool, int, int> updateQna;
private int InvalidMessageCount = 0;
string Query = string.Empty;
List<int> qnaIdStorage;
UserProfile userProfile = new UserProfile();
public QnADialog(Action<string, string, bool, int, int> updateQna, bool isCollection, List<int> rotationTemStorage, BotServices services, UserProfile _userProfile, IStatePropertyAccessor<UserProfile> userProfileAccessor, int invalidMessageCount = 0, string dialogId = null)
: base(Name)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_userProfileAccessor = userProfileAccessor ?? throw new ArgumentNullException(nameof(userProfileAccessor));
userProfile = _userProfile;
this.updateQna = updateQna;
this.InvalidMessageCount = invalidMessageCount;
qnaIdStorage = rotationTemStorage;
var waterfallSteps = new WaterfallStep[]
{
BeginStepAsync,
FetchFAQResultStepAsync,
FeedbackStepAsync,
FeedbackResponseStepAsync,
};
AddDialog(new WaterfallDialog(QnAFeedbackDialog, waterfallSteps));
AddDialog(new TextPrompt("userFeed"));
}
public async Task<DialogTurnResult> BeginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
var messageToForward = stepContext.Context.Activity;
UserProfile.previousQuestion = messageToForward.Text;
string[] supportList = { "HELP", "FEEDBACK", "SUPPORT", "ESCALATE", "AGENT" };
if (messageToForward.Text == null || messageToForward.Text.ToLower() == "no")
{
await stepContext.Context.SendActivityAsync("Sorry, I was not able to help you.");
return await stepContext.EndDialogAsync();
}
else if (messageToForward.Text == null || supportList.Any(x => x == messageToForward.Text.ToUpper()))
{
await stepContext.Context.SendActivityAsync("Please reach out to... ");
return await stepContext.EndDialogAsync();
}
else
{
return await stepContext.NextAsync();
}
}
private async Task<DialogTurnResult> FetchFAQResultStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var message = stepContext.Context.Activity;
var qnaResult = await FaqQnaMakerService.GetQnaResult(_services, stepContext, this.Query);
var qnaIdColl = GetQnaIdColl(this.Query, qnaResult);
int qnaPreviousId = 0;
int qnaNewId = 0;
if (qnaIdColl != null && qnaIdColl.Count > 1)
{
qnaIdColl = qnaIdColl.Distinct().OrderBy(x => x).ToList();
//Compare the previous Qnaid collection and existing collection , if it is matching produce the result.
var matchItem = qnaIdColl.Intersect(qnaIdStorage);
if (matchItem.Count() == 0)
{
//If there is no previous collection Qna id then take the first item from the existing Qna collection
qnaNewId = qnaIdColl.FirstOrDefault();
}
else
{
//If there any previous Qnaid that contain in the existing collection then pick the next value and generate a new qna result.
qnaPreviousId = matchItem.FirstOrDefault();
qnaNewId = GetNextRotationKey(qnaIdColl, qnaPreviousId);
}
//Create a new response based on selected new qna id.
qnaResult = new[] { qnaResult.Where(x => x.Id == qnaNewId).Single() };
}
if (qnaResult.First().Answer.Length > 0)
{
if (qnaResult.First().Score > 0)
{
updateQna(this.Query, qnaResult.First().Answer, false, qnaPreviousId, qnaNewId);
InvalidMessageCount = 0;
var QuestionCollection = TextFormatter.FormattedQuestionColl(qnaResult.First().Answer);
if (QuestionCollection != null)
{
userProfile.IsAswerCollection = true;
updateQna(this.Query, qnaResult.First().Answer, true, qnaPreviousId, qnaNewId);
var replyMessage = stepContext.Context.Activity.CreateReply();
replyMessage.Attachments = new List<Attachment>() { AllAdaptiveCard.QnaAttachment(new Tuple<string, string[]>(QuestionCollection.Item2, QuestionCollection.Item3)) };
if (!string.IsNullOrEmpty(QuestionCollection.Item1))
{
await stepContext.Context.SendActivityAsync(QuestionCollection.Item1);
}
await stepContext.Context.SendActivityAsync(replyMessage);
return await stepContext.EndDialogAsync();
}
else
{
await stepContext.Context.SendActivityAsync(qnaResult.First().Answer);
}
}
else
{
InvalidMessageCount++;
return await stepContext.ContinueDialogAsync();
}
}
return await stepContext.NextAsync();
}
private async Task<DialogTurnResult> FeedbackStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync("userFeed", new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply("Did this help?")
});
}
private async Task<DialogTurnResult> FeedbackResponseStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var message = stepContext.Context.Activity;
var mesgActivity = message as Activity;
string var = userProfile.qnaData;
var qnaResultModel = new { InvalidMessageCount = 0, originalQueryText = string.Empty };
NeedMoreInformation needmoreInfo = NeedMoreInformation.NotSelected;
if (message != null && message.Text == null && message.Value != null)
{
dynamic value = mesgActivity.Value.ToString();
UserReply response = JsonConvert.DeserializeObject<UserReply>(value);
if (!string.IsNullOrEmpty(response.Reply))
{
mesgActivity.Text = response.Reply;
}
}
//This if condition work only the user reply back to the question "Did this help?"
if (userProfile.needMoreInformation == true && message?.Text?.ToLower() != "yes" && message?.Text?.ToLower() != "no")
{
//The response message pass to LUIS service to understand the intention of the conversation is βyesβ or βnoβ
bool? moreInformationYes = await LUISService.GetResultAESChatBotYesNo(message?.Text);
if (moreInformationYes != null && moreInformationYes == true)
{
//Once the LUIS understand the conversation change the original message to yes.
message.Text = "yes";
//needmoreInfo = NeedMoreInformation.Yes;
}
else if (moreInformationYes != null && moreInformationYes == false)
{
////Once the LUIS understand the conversation change the original message to no.
message.Text = "no";
needmoreInfo = NeedMoreInformation.No;
}
else
{
needmoreInfo = NeedMoreInformation.None;
}
}
if (userProfile.needMoreInformation == true && message?.Text?.ToLower() == "yes")
{
userProfile.qnaInvalidMessageCount = 0;
userProfile.needMoreInformation = false;
dynamic value = stepContext.Context.Activity.Value;
var output = JsonConvert.DeserializeObject<UserReply>(stepContext.Context.Activity.Value.ToString());
if (userProfile.feedbackCard == false)
{
var replyMessage = stepContext.Context.Activity.CreateReply();
replyMessage.Attachments = new List<Attachment>() { AllAdaptiveCard.FeedbackAdapativecard() };
await stepContext.Context.SendActivityAsync(replyMessage);
}
if (output.Reply != "yes")
{
await AdaptiveCardReplyAsync(_services, stepContext, userProfile);
}
}
else if (userProfile.needMoreInformation == true && message?.Text?.ToLower() == "no")
{
userProfile.qnaInvalidMessageCount = 0;
userProfile.needMoreInformation = false;
dynamic value = stepContext.Context.Activity.Value;
if (value.Type == "GetMoreContent")
{
await AdaptiveCardGetMoreContent(_services, stepContext, userProfile);
}
else if (value.Type == "GetHelpSubmit")
{
await AdaptiveCardReplyAsync(_services, stepContext, userProfile);
}
else if (userProfile.getMoreContentCard == false)
{
var replyMessage = stepContext.Context.Activity.CreateReply();
replyMessage.Attachments = new List<Attachment>() { AllAdaptiveCard.GetMoreContent() };
await stepContext.Context.SendActivityAsync(replyMessage);
}
// context.Wait(AdaptiveCardGetMoreContent);
}
else
{
await stepContext.BeginDialogAsync(nameof(Bot.cs));
}
return await stepContext.EndDialogAsync();
}
}
After this prompt it should go to the next step as added in the waterfall steps but it does not. Any possible suggestions/help would be greatly appreciated. Thanks in advance!
Without seeing the code for your other steps inside the Waterfall such as BeginStepAsync and FetchFAQResultStepAsync it is difficult to give you an exact answer for your scenario.
How I would suggest you accomplish this is through the use of a message with suggested actions underneath this message, once either of this actions is clicked both options will disappear, thus removing the potential for multiple submissions by the same user for the same answer reply.
You have a couple of options here:
1) Use this dated sample that uses v3.9.0 of the Microsoft.Bot.Builder NuGet package, the meat of which is in the QnADialog and FeedbackDialog classes.
The important part is that the QnADialog implements QnAMakerDialog.
2) Right after where you send the reply to the user with the answer (inside FetchFAQResultsStepAsync I assume) you could add the following code:
var feedback = ((Activity)context.Activity).CreateReply("Did you find what you need?");
feedback.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
new CardAction(){ Title = "Yes", Type=ActionTypes.PostBack, Value=$"yes-positive-feedback" },
new CardAction(){ Title = "No", Type=ActionTypes.PostBack, Value=$"no-negative-feedback" }
}
};
await context.PostAsync(feedback);
EDIT
Thank you for providing the full code for your QnADialog class, unfortunately I cannot run it locally because the implementations for methods such as GetQnaIdColl, GetNextRotationKey, TextFormatter.FormattedQuestionColl among other methods and classes that you call but haven't provided. Your code for prompting a user for a response looks right but it sounds like you're not even getting the feedback prompt to show, or you're getting the feedback prompt to show but you get stuck on there - can you confirm which it is? Have you tried stepping through the code to see which path it takes?
Another suggestion would be to separate out your QnA step and Feedback steps into separate Dialogs, I have proved and example feedback dialog below.
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using System.Threading;
using System.Threading.Tasks;
namespace ChatBot.VirtualAssistant.Dialogs
{
public class RateAnswerDialog : ComponentDialog
{
public RateAnswerDialog()
: base(nameof(RateAnswerDialog))
{
InitialDialogId = nameof(RateAnswerDialog);
var askToRate = new WaterfallStep[]
{
AskRating,
FinishDialog
};
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new WaterfallDialog(InitialDialogId, askToRate));
}
private async Task<DialogTurnResult> AskRating(WaterfallStepContext sc, CancellationToken cancellationToken)
{
PromptOptions promptOptions = new PromptOptions
{
Prompt = MessageFactory.Text("Was this helpful?")
};
return await sc.PromptAsync(nameof(TextPrompt), promptOptions);
}
private async Task<DialogTurnResult> FinishDialog(WaterfallStepContext sc, CancellationToken cancellationToken)
{
return await sc.EndDialogAsync(sc);
}
protected override async Task<DialogTurnResult> EndComponentAsync(DialogContext outerDc, object context, CancellationToken cancellationToken)
{
var waterfallContext = (WaterfallStepContext)context;
var userResponse = ((string)waterfallContext.Result).ToLowerInvariant();
if (userResponse == "yes")
{
await waterfallContext.Context.SendActivityAsync("Thank you for your feedback");
}
else if (userResponse == "no")
{
await waterfallContext.Context.SendActivityAsync("Sorry I couldn't help you");
}
else
{
await waterfallContext.Context.SendActivityAsync("The valid answers are 'yes' or 'no'");
// TODO reprompt if required
}
return await outerDc.EndDialogAsync();
}
}
}
I have this function that updates a CouchDB Database but I want it to try updating again if the Response Code is conflict, i want it to have 3 tries, how do I do that?
public async Task<HttpResponseMessage> UpdateRecord(Profile latestProfile)
{
ProfileRecordByUpn profileRecord = await this.GetProfileByUpn(latestProfile);
Profile oldProfile = profileRecord.Rows.First().Value;
var client = this.clientFactory.CreateClient(NamedHttpClients.COUCHDB);
var formatter = new JsonMediaTypeFormatter();
formatter.SerializerSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var query = HttpUtility.ParseQueryString(string.Empty);
query["rev"] = oldProfile.Rev;
//Setting the profile Active = true, because as of now we don't have any UI for disabling the account
latestProfile.Active = oldProfile.Active;
DateTimeOffset now = DateTimeOffset.Now;
latestProfile.Created = oldProfile.Created;
latestProfile.Modified = now;
//This will check if we the InApp boolean value changed then will set date to Enabled/Disabled
if (oldProfile.InApp != latestProfile.InApp)
{
if (latestProfile.InApp == true)
{
latestProfile.InAppEnabled = now;
latestProfile.InAppDisabled = oldProfile.InAppDisabled;
}
else
{
latestProfile.InAppDisabled = now;
latestProfile.InAppEnabled = oldProfile.InAppEnabled;
}
}
else
{
latestProfile.InAppEnabled = oldProfile.InAppEnabled;
latestProfile.InAppDisabled = oldProfile.InAppDisabled;
}
//This will check if we the SMS boolean value changed then will set date to Enabled/Disabled
if (oldProfile.SMS != latestProfile.SMS)
{
if (latestProfile.SMS == true)
{
latestProfile.SMSEnabled = now;
latestProfile.SMSDisabled = oldProfile.SMSDisabled;
}
else
{
latestProfile.SMSDisabled = now;
latestProfile.SMSEnabled = oldProfile.SMSEnabled;
}
}
else
{
latestProfile.SMSEnabled = oldProfile.SMSEnabled;
latestProfile.SMSDisabled = oldProfile.SMSDisabled;
}
//This will check if we the SMS boolean value changed then will set date to Enabled/Disabled
if (oldProfile.Email != latestProfile.Email)
{
if (latestProfile.Email == true)
{
latestProfile.EmailEnabled = now;
latestProfile.EmailDisabled = oldProfile.EmailDisabled;
}
else
{
latestProfile.EmailDisabled = now;
latestProfile.EmailEnabled = oldProfile.EmailEnabled;
}
}
else
{
latestProfile.EmailEnabled = oldProfile.EmailEnabled;
latestProfile.EmailDisabled = oldProfile.EmailDisabled;
}
var response = await this.couchDbClient.AuthenticatedQuery(async (c) => {
return await c.PutAsync($"{API_PROFILES_DB}/{oldProfile.Id.ToString()}?{query}", latestProfile, formatter);
}, NamedHttpClients.COUCHDB, client);
return response;
}
so I will be calling this function from another function? Do I make an another function which is a higher order function and pass this function as a parameter to that higher order function?
Higher-order functions in C# are implemented by methods taking delegates as parameters, usually an Action or Func delegate.
In this case, you should use an established library like Polly.
var policy = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.Conflict)
.RetryAsync(3);
var result = await policy.ExecuteAsync(() => UpdateRecord(latestProfile));
Update to do it yourself (uncompiled and untested code):
async Task<HttpResponseMessage> MyRetry(Func<Task<HttpResponseMessage>> action)
{
for (int retries = 0; retries < 3; ++retries)
{
var result = await action();
if (result.StatusCode != HttpStatusCode.Conflict)
return result;
}
return await action();
}
The above code will retry 3 times, for 4 total calls if it keeps returning Conflict.
Bot Info
SDK Platform: .NET
SDK Version: 3.14.0.7
Active Channels: Web
Deployment Environment: Local development with Emulator
Issue Description
We've trying to unit test every case that we have stored in a certain Dictionary, it seems to be working fine when the user sends and string and the test has to answer with a string. But we can't find any documentation on how to test the other kind of dialogs, like with attachments, buttons, etc.
We wish to make a dictionary of string,objects where the string is what we ask the bot and de object is either a string, Attachment, Dialog.
Code Example
This is how we store the answers:
public static Dictionary<string, object> data = new Dictionary<string, object>{
{"Nuevo", "Que quieres crear?"},
{"Ayuda", "Ya te ayudas!"},
{"Adios", "Nos vemos!"},
{
"Coche",
new Attachment() {
ContentUrl = "https://media.ed.edmunds-media.com/subaru/impreza/2006/oem/2006_subaru_impreza_sedan_sti_fq_oem_1_500.jpg",
ContentType = "image/png",
Name = "Subaru_Impreza.png"
}
},
{
"Moto",
new Attachment() {
ContentUrl = "http://motos.honda.com.co/sites/default/files/motos/cb-1000-r-cc-menu-honda.png",
ContentType = "image/png",
Name = "moto.png"
}
},
{
"Perro",
new Attachment() {
ContentUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Taka_Shiba.jpg/1200px-Taka_Shiba.jpg",
ContentType = "image/png",
Name = "ShibaInu.png"
}
}
};
This is how the bot works and returns everything, this is working as intended for at least text and attachments but we haven't done it for more type of messages.
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
var r = context.MakeMessage();
foreach (var item in data)
{
if (item.Key == activity.Text)
{
if (item.Value is Attachment)
{
r.Attachments = new List<Attachment>() { item.Value as Attachment };
}
if (item.Value is string)
{
r.Text = item.Value.ToString();
}
break;
}
}
// return our reply to the user
await context.PostAsync(r);
context.Wait(MessageReceivedAsync);
}
But when we want to make the test for it, it only works when what we send is a string not a IMessageActivity, which works in the emulator.
The code for the test:
[TestMethod]
public async Task Pregunta_respuesta_prueba()
{
foreach (var item in RootDialog.data)
{
var preg = item.Key;
var resp = item.Value;
if (item.Value is Attachment)
{
Attachment auxText = resp as Attachment;
resp = auxText.ContentUrl;
}
using (ShimsContext.Create())
{
// Arrange
var waitCalled = false;
object message = null;
var target = new RootDialog();
var activity = new Activity(ActivityTypes.Message)
{
Text = preg
};
var awaiter = new Microsoft.Bot.Builder.Internals.Fibers.Fakes.StubIAwaiter<IMessageActivity>()
{
IsCompletedGet = () => true,
GetResult = () => activity
};
var awaitable = new Microsoft.Bot.Builder.Dialogs.Fakes.StubIAwaitable<IMessageActivity>()
{
GetAwaiter = () => awaiter
};
var context = new Microsoft.Bot.Builder.Dialogs.Fakes.StubIDialogContext();
Microsoft.Bot.Builder.Dialogs.Fakes.ShimExtensions.PostAsyncIBotToUserStringStringCancellationToken = (user, s1, s2, token) =>
{
message = s1;
Console.WriteLine(message);
return Task.CompletedTask;
};
Microsoft.Bot.Builder.Dialogs.Fakes.ShimExtensions.WaitIDialogStackResumeAfterOfIMessageActivity = (stack, callback) =>
{
if (waitCalled) return;
waitCalled = true;
// The callback is what is being tested.
callback(context, awaitable);
};
// Act
await target.StartAsync(context);
// Assert
Assert.AreEqual(resp, message);
}
}
}
If you check this part of the code
Microsoft.Bot.Builder.Dialogs.Fakes.ShimExtensions.PostAsyncIBotToUserStringStringCancellationToken = (user, s1, s2, token) =>
{
message = s1;
Console.WriteLine(message);
return Task.CompletedTask;
};
```
It does only works when the bot is returning an string, we can't even check if it is an activiy, this happens because the Fake Context that we create for the test is not working as expected.
That IDialogContext that we are faking doesnt seem to work at all when it is an object, but it does work when it is a string.
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
/// Here when the test is running, this context.MakeMessage is null, but when the bot
/// is working, it wors perfectly.
var r = context.MakeMessage();
foreach (var item in data)
{
if (item.Key == activity.Text)
{
if (item.Value is Attachment)
{
r.Attachments = new List<Attachment>() { item.Value as Attachment };
}
if (item.Value is string)
{
r.Text = item.Value.ToString();
}
break;
}
}
// return our reply to the user
await context.PostAsync(r);
context.Wait(MessageReceivedAsync);
}
Reproduction Steps
To try this out you can try to test with an attachment, code is in this repository.
In stead of using PostAsyncIBotToUserStringStringCancellationToken, you can use context.PostAsyncIMessageActivityCancellationToken And, in the RootDialog's MessageReceivedWithTextAsync respond with an activity reply instead of just a string.
public async Task MessageReceivedWithTextAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
string r = "";
foreach (var item in dataText)
{
if (item.Key == activity.Text)
{
r = item.Value;
break;
}
}
var reply = activity.CreateReply(r);
foreach (var item in dataAtt)
{
if (item.Key == activity.Text)
{
reply.Attachments.Add(item.Value);
reply.Text = "attachment";
break;
}
}
if ((string.IsNullOrWhiteSpace(r) || r == null) && reply.Attachments.Count == 0)
{
reply.Text = "No tengo respuesta para eso.";
}
// return our reply to the user
await context.PostAsync(reply);
}
Here are the changes to the test method:
[TestMethod]
public async Task Bot_Test_Attachments()
{
foreach (var item in RootDialog.dataAtt)
{
var preg = item.Key;
var att = item.Value;
using (ShimsContext.Create())
{
var waitCalled = false;
IMessageActivity message = null;
var target = new RootDialog();
var activity = new Activity(ActivityTypes.Message)
{
Text = preg,
From = new ChannelAccount("id","name"),
Recipient = new ChannelAccount("recipid","recipname"),
Conversation = new ConversationAccount(false,"id","name")
};
var awaiter = new Microsoft.Bot.Builder.Internals.Fibers.Fakes.StubIAwaiter<IMessageActivity>()
{
IsCompletedGet = () => true,
GetResult = () => activity
};
var awaitable = new Microsoft.Bot.Builder.Dialogs.Fakes.StubIAwaitable<IMessageActivity>()
{
GetAwaiter = () => awaiter
};
var context = new Microsoft.Bot.Builder.Dialogs.Fakes.StubIDialogContext();
context.PostAsyncIMessageActivityCancellationToken = (messageActivity, token) => {
message = messageActivity;
return Task.CompletedTask;
};
Microsoft.Bot.Builder.Dialogs.Fakes.ShimExtensions.WaitIDialogStackResumeAfterOfIMessageActivity = (stack, callback) =>
{
if (waitCalled) return;
waitCalled = true;
callback(context, awaitable);
};
await target.MessageReceivedWithTextAsync(context, awaitable);
Assert.AreEqual(att, message.Attachments[0]);
}
}
}