I have this code in OnTurnAsync() in the main bot class that let the user upload image and saved it locally and its working fine.
But how can i do this if i want to do it in a different class like a MainDialog class?
Update: I manage to do it by the code below. But it is accepting everything, how can i do a validation that accepts only images?
Update: Answer below.
public class MainDialog : ComponentDialog
{
private const string InitialId = "mainDialog";
private const string TEXTPROMPT = "textPrompt";
private const string ATTACHPROMPT = "attachPrompt";
public MainDialog(string dialogId)
: base(dialogId)
{
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new AttachmentPrompt(ATTACHPROMPT));
}
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
return await stepContext.PromptAsync(
ATTACHPROMPT,
new PromptOptions
{
Prompt = MessageFactory.Text($"upload photo."),
RetryPrompt = MessageFactory.Text($"upload photo pls."),
});
}
private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
var activity = stepContext.Context.Activity;
if (activity.Attachments != null && activity.Attachments.Any())
{
foreach (var file in stepContext.Context.Activity.Attachments)
{
var remoteFileUrl = file.ContentUrl;
var localFileName = Path.Combine(Path.GetTempPath(), file.Name);
using (var webClient = new WebClient())
{
webClient.DownloadFile(remoteFileUrl, localFileName);
}
await stepContext.Context.SendActivityAsync($"Attachment {stepContext.Context.Activity.Attachments[0].Name} has been received and saved to {localFileName}.");
}
}
return await stepContext.EndDialogAsync();
}
Managed to do it with this code.
var activity = stepContext.Context.Activity;
if (activity.Attachments != null && activity.Attachments.Any())
{
foreach (var file in stepContext.Context.Activity.Attachments)
{
if (file.ContentType.EndsWith("/jpg") || file.ContentType.EndsWith("/png") || file.ContentType.EndsWith("/bmp") || file.ContentType.EndsWith("/jpe"))
{
var remoteFileUrl = file.ContentUrl;
var localFileName = Path.Combine(Path.GetTempPath(), file.Name);
using (var webClient = new WebClient())
{
webClient.DownloadFile(remoteFileUrl, localFileName);
}
await stepContext.Context.SendActivityAsync($"Attachment {stepContext.Context.Activity.Attachments[0].Name} has been received and saved to {localFileName}.");
return await stepContext.NextAsync();
}
else
{
await stepContext.Context.SendActivityAsync($"Attachment {file.Name} is not an image, it is {file.ContentType}.");
// restart the dialog from top
return await stepContext.ReplaceDialogAsync(InitialId);
}
}
}
Like Nicolas R said, you could do some basic validation like this:
foreach (var file in activity.Attachments)
{
if (file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
await turnContext.SendActivityAsync($"Attachment {file.Name} is an image of type {file.ContentType}.");
}
else
{
await turnContext.SendActivityAsync($"Attachment {file.Name} is not an image, it is {file.ContentType}.");
}
}
This will of course allow any attachment with a content type that starts with "image/". You might not want to allow any content type that starts with "image/", in which case you'd want to make a list of accepted types like "image/png" and "image/jpeg". You might also consider checking the file extension (to see if it's ".png" or ".jpg" etc.)
Related
I'm trying to generate a zip file of pdfs asynchronously to speed things up as follows:
var files = new Dictionary<string, byte[]>();
var fileTasks = new List<Task<Library.Models.Helpers.File>>();
foreach (var i in groups)
{
var task = Task.Run(async () =>
{
var fileName = $"{i.Key.Title.Replace('/', '-')} - Records.pdf";
ViewBag.GroupName= i.Key.Title;
var html = await this.RenderViewAsync("~/Views/Report/_UserRecordsReport.cshtml", i.ToList(), true);
return await _fileUtilityService.HtmlToPDF2(html, null, fileName);
});
fileTasks.Add(task);
}
var completedTaskFiles = await Task.WhenAll(fileTasks);
foreach(var item in completedTaskFiles)
{
files.Add($"{item.FileName}", item.FileResult);
}
return _fileUtilityService.GenerateZIP(files);
I'm generating all my html to pdf file tasks and waiting for them to be completed - then trying to synchronously loop through the completed tasks and add them to my dictionary for zipping but I keep getting the following error:
An item with the same key has already been added
There is no duplicate key in the list of items being added.
EDIT - so the current idea is that because its a scoped service, thats why i'm running into thread issues (attached the file utility service for information)
public class FileUtilityService : IFileUtilityService
{
private readonly IHttpClientFactory _clientFactory;
public FileUtilityService(IHttpClientFactory clientFactory)
{
public async Task<byte[]> HtmlToPDF(string html = null, string url = null)
{
try
{
byte[] res = null;
if (html is null && url != null)
{
var client = _clientFactory.CreateClient();
var requestResp = await client.GetAsync(url);
using var sr = new StreamReader(await requestResp.Content.ReadAsStreamAsync());
html = HttpUtility.HtmlDecode(await sr.ReadToEndAsync());
}
using(var ms = new MemoryStream())
{
HtmlConverter.ConvertToPdf(html, ms);
res = ms.ToArray();
}
return res;
}catch(Exception ex)
{
throw ex;
}
}
public async Task<Library.Models.Helpers.File> HtmlToPDF(string html = null, string url = null, string fileName = "")
{
return new Library.Models.Helpers.File() { FileName = fileName, FileResult = await HtmlToPDF(html, url) };
}
I've been trying to build a bot that uses QnA to anser the user based on categories.
I've managed to connect to QnA correctly and get the first level prompted to the user, however, the problem appears when I try to send to QnA another response from the user.
Here's the dialog that runs smoothly on it's first request.
private async Task<DialogTurnResult> InicioRRHHDialog(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
string intentForQna = IntentEnum.DISPATCH_RRHH;
var castContextForQna = (WaterfallStepContext)stepContext.Parent;
var contextForNextStep = stepContext.Context;
await GetQnaConnection(castContextForQna, intentForQna);
return await stepContext.NextAsync(contextForNextStep, cancellationToken);
}
This is the GetQnaConnection being called.
private async Task GetQnaConnection(WaterfallStepContext stepContext, string intent)
{
servicioQNA = new QnAMakerService(this._qnaService.Url, _qnaRRHHkbId, this._qnaService.QnaEndPointKey);
var mensaje = await servicioQNA.QueryQnAServiceAsync(intent, null);
await ShowResults(stepContext, mensaje);
}
And lastly the ShowResults method that I use to send the activity back to the user.
private static async Task ShowResults(WaterfallStepContext stepContext, QnAResult[] mensaje)
{
Activity outputActivity = null;
QnABotState newState = null;
var qnaAnswer = mensaje[0].Answer;
var prompts = mensaje[0].Context?.Prompts;
if (prompts == null || prompts.Length < 1)
outputActivity = MessageFactory.Text(qnaAnswer);
else
{
newState = new QnABotState
{
PreviousQnaId = mensaje[0].Id,
PreviousUserQuery = stepContext.Result.ToString()
};
outputActivity = CardHelper.GetHeroCard(qnaAnswer, prompts);
}
var outputs = new Activity[] { outputActivity };
foreach (var activity in outputs)
await stepContext.Context.SendActivityAsync(activity).ConfigureAwait(false);
}
Am I handling the Qna connection correctly?
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();
}
}
}
so I have this code below I have adapted it to use only one card need, I would like help to remove the random function as when I click action.submit on the card it brings up the same card again I would like this card to be displayed just once and end the conversation with thank you when action.submit is pressed.
I have followed along with lots of documentation and tutorials but some are out of date and some don't explain the method fully. I have really tired myself and would just like a bit of guidance to learn, thank you for any help.
public class AdaptiveCardsBot : IBot
{
private const string WelcomeText = #"Type anything to see the prototype.";
// This array contains the file location of our adaptive cards
private readonly string[] _cards =
{
Path.Combine(".", "Resources", "card.json"),
};
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (turnContext.Activity.Type == ActivityTypes.Message)
{
Random r = new Random();
var cardAttachment = CreateAdaptiveCardAttachment(this._cards[r.Next(this._cards.Length)]);
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>() { cardAttachment };
await turnContext.SendActivityAsync(reply, cancellationToken);
}
else if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
{
if (turnContext.Activity.MembersAdded != null)
{
await SendWelcomeMessageAsync(turnContext, cancellationToken);
}
}
else
{
await turnContext.SendActivityAsync($"{turnContext.Activity.Type} activity detected", cancellationToken: cancellationToken);
}
}
private static async Task SendWelcomeMessageAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(
$"Welcome to This Adaptive card Prototype. {WelcomeText}",
cancellationToken: cancellationToken);
}
}
}
private static Attachment CreateAdaptiveCardAttachment(string filePath)
{
var adaptiveCardJson = File.ReadAllText(filePath);
var adaptiveCardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(adaptiveCardJson),
};
return adaptiveCardAttachment;
}
}
expected results are single adaptive card shown data collected, action. submit pressed, data submitted and a thank you message.
You can eliminate the random function removing this instruction.
r.Next(this._cards.Length)
In the line:
var cardAttachment = CreateAdaptiveCardAttachment(this._cards[r.Next(this._cards.Length)]);
This is the card's array:
private readonly string[] _cards =
{
Path.Combine(".", "Resources", "FlightItineraryCard.json"),
Path.Combine(".", "Resources", "ImageGalleryCard.json"),
Path.Combine(".", "Resources", "LargeWeatherCard.json"),
Path.Combine(".", "Resources", "RestaurantCard.json"),
Path.Combine(".", "Resources", "SolitaireCard.json"),
};