/// <summary> /// Get the results from Azure search service and populate the result (card + preview). /// </summary> /// <param name="languageCode">Current expert team's applicable language code.</param> /// <param name="query">Query which the user had typed in message extension search.</param> /// <param name="commandId">Command id to determine which tab in message extension has been invoked.</param> /// <param name="count">Count for pagination.</param> /// <param name="skip">Skip for pagination.</param> /// <param name="localTimestamp">Local timestamp of the user activity.</param> /// <param name="searchService">Search service.</param> /// <param name="knowledgeBaseSearchService">Knowledgebase search service.</param> /// <param name="activityStorageProvider">Activity storage provider.</param> /// <param name="appBaseUri">Application base uri.</param> /// <returns><see cref="Task"/> Returns MessagingExtensionResult which will be used for providing the card.</returns> public static async Task <MessagingExtensionResult> GetSearchResultAsync( string languageCode, string query, string commandId, int?count, int?skip, DateTimeOffset?localTimestamp, ISearchService searchService, IKnowledgeBaseSearchService knowledgeBaseSearchService, IActivityStorageProvider activityStorageProvider, string appBaseUri) { MessagingExtensionResult composeExtensionResult = new MessagingExtensionResult { Type = "result", AttachmentLayout = AttachmentLayoutTypes.List, Attachments = new List <MessagingExtensionAttachment>(), }; IList <TicketEntity> searchServiceResults = new List <TicketEntity>(); // commandId should be equal to Id mentioned in Manifest file under composeExtensions section. switch (commandId) { case RecentCommandId: searchServiceResults = await searchService.SearchTicketsAsync(TicketSearchScope.RecentTickets, languageCode, query, count, skip).ConfigureAwait(false); composeExtensionResult = GetMessagingExtensionResult(commandId, localTimestamp, searchServiceResults, appBaseUri); break; case OpenCommandId: searchServiceResults = await searchService.SearchTicketsAsync(TicketSearchScope.UnAnsweredTickets, languageCode, query, count, skip).ConfigureAwait(false); composeExtensionResult = GetMessagingExtensionResult(commandId, localTimestamp, searchServiceResults, appBaseUri); break; case AssignedCommandId: searchServiceResults = await searchService.SearchTicketsAsync(TicketSearchScope.AnsweredTickets, languageCode, query, count, skip).ConfigureAwait(false); composeExtensionResult = GetMessagingExtensionResult(commandId, localTimestamp, searchServiceResults, appBaseUri); break; case KnowledgebaseQuestionCommandId: var azureSearchEntities = await knowledgeBaseSearchService.GetAzureSearchEntitiesAsync(query).ConfigureAwait(false); if (azureSearchEntities.Any()) { var activitiesData = await activityStorageProvider.GetActivityEntitiesAsync().ConfigureAwait(false); composeExtensionResult.Attachments = MessagingExtensionQnaCard.GetAllKbQuestionsCard(azureSearchEntities, activitiesData); } break; } return(composeExtensionResult); }
/// <summary> /// Method perform update operation of question and answer pair. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="answer">Answer of the given question.</param> /// <param name="qnaPairEntity">Qna pair entity that contains question and answer information.</param> /// <returns>A <see cref="Task"/> of type bool where true represents question and answer pair updated successfully while false indicates failure in updating the question and answer pair.</returns> public async Task<bool> SaveQnAPairAsync(ITurnContext turnContext, string answer, AdaptiveSubmitActionData qnaPairEntity) { QnASearchResult searchResult; var qnaAnswerResponse = await this.qnaServiceProvider.GenerateAnswerAsync(qnaPairEntity.OriginalQuestion, qnaPairEntity.IsTestKnowledgeBase).ConfigureAwait(false); searchResult = qnaAnswerResponse.Answers.FirstOrDefault(); bool isSameQuestion = false; // Check if question exist in the knowledgebase. if (searchResult != null && searchResult.Questions.Count > 0) { // Check if the edited question & result returned from the knowledgebase are same. isSameQuestion = searchResult.Questions.First().ToUpperInvariant() == qnaPairEntity.OriginalQuestion.ToUpperInvariant(); } // Edit the QnA pair if the question is exist in the knowledgebase & exactly the same question on which we are performing the action. if (searchResult.Id != -1 && isSameQuestion) { int qnaPairId = searchResult.Id.Value; await this.qnaServiceProvider.UpdateQnaAsync(qnaPairId, answer, turnContext.Activity.From.AadObjectId, qnaPairEntity.UpdatedQuestion, qnaPairEntity.OriginalQuestion).ConfigureAwait(false); this.logger.LogInformation($"Question updated by: {turnContext.Activity.Conversation.AadObjectId}"); Attachment attachment = new Attachment(); if (qnaPairEntity.IsRichCard) { qnaPairEntity.IsPreviewCard = false; qnaPairEntity.IsTestKnowledgeBase = true; attachment = MessagingExtensionQnaCard.ShowRichCard(qnaPairEntity, turnContext.Activity.From.Name, Strings.LastEditedText); } else { qnaPairEntity.IsTestKnowledgeBase = true; qnaPairEntity.Description = answer ?? throw new ArgumentNullException(nameof(answer)); attachment = MessagingExtensionQnaCard.ShowNormalCard(qnaPairEntity, turnContext.Activity.From.Name, actionPerformed: Strings.LastEditedText); } var activityId = this.activityStorageProvider.GetAsync(qnaAnswerResponse.Answers.First().Metadata.FirstOrDefault(x => x.Name == Constants.MetadataActivityReferenceId)?.Value).Result.FirstOrDefault().ActivityId; var updateCardActivity = new Activity(ActivityTypes.Message) { Id = activityId ?? throw new ArgumentNullException(nameof(activityId)), Conversation = turnContext.Activity.Conversation, Attachments = new List<Attachment> { attachment }, }; // Send edited question and answer card as response. await turnContext.UpdateActivityAsync(updateCardActivity, cancellationToken: default).ConfigureAwait(false); } else { return false; } return true; }
/// <summary> /// Return card response. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="postedQnaPairEntity">Qna pair entity that contains question and answer information.</param> /// <param name="answer">Answer text.</param> /// <returns>Card attachment.</returns> private async Task<Attachment> CardResponseAsync( ITurnContext<IInvokeActivity> turnContext, AdaptiveSubmitActionData postedQnaPairEntity, string answer) { Attachment qnaAdaptiveCard = new Attachment(); bool isSaved; if (postedQnaPairEntity.UpdatedQuestion?.ToUpperInvariant().Trim() == postedQnaPairEntity.OriginalQuestion?.ToUpperInvariant().Trim()) { postedQnaPairEntity.IsTestKnowledgeBase = false; isSaved = await this.SaveQnAPairAsync(turnContext, answer, postedQnaPairEntity).ConfigureAwait(false); if (!isSaved) { postedQnaPairEntity.IsTestKnowledgeBase = true; await this.SaveQnAPairAsync(turnContext, answer, postedQnaPairEntity).ConfigureAwait(false); } } else { // Check if question exist in the production/test knowledgebase & exactly the same question. var hasQuestionExist = await this.qnaServiceProvider.QuestionExistsInKbAsync(postedQnaPairEntity.UpdatedQuestion).ConfigureAwait(false); // Edit the question if it doesn't exist in the test knowledgebse. if (hasQuestionExist) { // If edited question text is already exist in the test knowledgebase. postedQnaPairEntity.IsQuestionAlreadyExists = true; } else { // Save the edited question in the knowledgebase. postedQnaPairEntity.IsTestKnowledgeBase = false; isSaved = await this.SaveQnAPairAsync(turnContext, answer, postedQnaPairEntity).ConfigureAwait(false); if (!isSaved) { postedQnaPairEntity.IsTestKnowledgeBase = true; await this.SaveQnAPairAsync(turnContext, answer, postedQnaPairEntity).ConfigureAwait(false); } } if (postedQnaPairEntity.IsQuestionAlreadyExists) { // Response with question already exist(in test knowledgebase). qnaAdaptiveCard = MessagingExtensionQnaCard.AddQuestionForm(postedQnaPairEntity, this.appBaseUri); } } return qnaAdaptiveCard; }
/// <summary> /// Handles "Add new question" button via messaging extension. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="action">Action to be performed.</param> /// <param name="cancellationToken">Propagates notification that operations should be canceled.</param> /// <returns>Response of messaging extension action.</returns> public async Task <MessagingExtensionActionResponse> FetchTaskAsync( ITurnContext <IInvokeActivity> turnContext, MessagingExtensionAction action, CancellationToken cancellationToken) { if (turnContext == null) { throw new ArgumentNullException(nameof(turnContext)); } else if (action == null) { throw new ArgumentNullException(nameof(action)); } try { turnContext.Activity.TryGetChannelData <TeamsChannelData>(out var teamsChannelData); string expertTeamId = this.configurationProvider.GetSavedEntityDetailAsync(ConfigurationEntityTypes.TeamId).GetAwaiter().GetResult(); if (teamsChannelData?.Team?.Id != expertTeamId) { var unauthorizedUserCard = MessagingExtensionQnaCard.UnauthorizedUserActionCard(); return(new MessagingExtensionActionResponse { Task = new TaskModuleContinueResponse { Value = new TaskModuleTaskInfo { Card = unauthorizedUserCard ?? throw new ArgumentNullException(nameof(unauthorizedUserCard)), Height = 250, Width = 300, Title = Strings.AddQuestionSubtitle, }, }, }); }
/// <summary> /// Get the reply to a question asked by end user. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="message">Text message.</param> /// <returns>A task that represents the work queued to execute.</returns> private async Task GetQuestionAnswerReplyAsync( ITurnContext turnContext, IMessageActivity message) { string text = message.Text?.ToLower()?.Trim() ?? string.Empty; try { var queryResult = new QnASearchResultList(); ResponseCardPayload payload = new ResponseCardPayload(); if (!string.IsNullOrEmpty(message.ReplyToId) && (message.Value != null)) { payload = ((JObject)message.Value).ToObject <ResponseCardPayload>(); } queryResult = await _qnaServiceProvider.GenerateAnswerAsync(question : text, isTestKnowledgeBase : false, payload.PreviousQuestions?.First().Id.ToString(), payload.PreviousQuestions?.First().Questions.First()).ConfigureAwait(false); if (queryResult.Answers.First().Id != -1) { var answerData = queryResult.Answers.First(); payload.QnaPairId = answerData.Id ?? -1; AnswerModel answerModel = new AnswerModel(); if (Validators.IsValidJSON(answerData.Answer)) { answerModel = JsonConvert.DeserializeObject <AnswerModel>(answerData.Answer); } if (!string.IsNullOrEmpty(answerModel?.Title) || !string.IsNullOrEmpty(answerModel?.Subtitle) || !string.IsNullOrEmpty(answerModel?.ImageUrl) || !string.IsNullOrEmpty(answerModel?.RedirectionUrl)) { await turnContext.SendActivityAsync(MessageFactory.Attachment(MessagingExtensionQnaCard.GetEndUserRichCard(text, answerData, payload.QnaPairId))).ConfigureAwait(false); } else { await turnContext.SendActivityAsync(MessageFactory.Attachment(ResponseCard.GetCard(answerData, text, _appBaseUri, payload))).ConfigureAwait(false); } _telemetryClient.TrackEvent( FaqPlusPlusBot.EVENT_ANSWERED_QUESTION_SINGLE, new Dictionary <string, string> { { "QuestionId", payload.QnaPairId.ToString() }, { "QuestionAnswered", queryResult.Answers[0].Questions[0] }, { "QuestionAsked", text }, { "UserName", turnContext.Activity.From.Name }, { "UserAadId", turnContext.Activity.From?.AadObjectId ?? "" }, { "Product", _options.ProductName }, }); } else { await turnContext.SendActivityAsync(MessageFactory.Attachment(UnrecognizedInputCard.GetCard(text))).ConfigureAwait(false); } } catch (Exception ex) { // Check if knowledge base is empty and has not published yet when end user is asking a question to bot. if (((Azure.CognitiveServices.Knowledge.QnAMaker.Models.ErrorResponseException)ex).Response.StatusCode == HttpStatusCode.BadRequest) { var knowledgeBaseId = await _configurationProvider.GetSavedEntityDetailAsync(Constants.KnowledgeBaseEntityId).ConfigureAwait(false); var hasPublished = await _qnaServiceProvider.GetInitialPublishedStatusAsync(knowledgeBaseId).ConfigureAwait(false); // Check if knowledge base has not published yet. if (!hasPublished) { this._telemetryClient.TrackException(ex, new Dictionary <string, string> { { "message", "Error while fetching the qna pair: knowledge base may be empty or it has not published yet." }, }); await turnContext.SendActivityAsync(MessageFactory.Attachment(UnrecognizedInputCard.GetCard(text))).ConfigureAwait(false); return; } } // Throw the error at calling place, if there is any generic exception which is not caught. throw; } }
/// <summary> /// Delete qna pair. /// </summary> /// <param name="turnContext">Turn context.</param> /// <param name="qnaServiceProvider">Qna Service provider.</param> /// <param name="activityStorageProvider">Activity Storage Provider.</param> /// <param name="logger">Logger.</param> /// <param name="cancellationToken">Cancellation Token.</param> /// <returns>A task that represents the work queued to execute.</returns> public static async Task DeleteQnaPair( ITurnContext <IMessageActivity> turnContext, IQnaServiceProvider qnaServiceProvider, IActivityStorageProvider activityStorageProvider, ILogger logger, CancellationToken cancellationToken) { QnASearchResult searchResult; Attachment attachment; var activity = (Activity)turnContext.Activity; var activityValue = ((JObject)activity.Value).ToObject <AdaptiveSubmitActionData>(); QnASearchResultList qnaAnswerResponse = await qnaServiceProvider.GenerateAnswerAsync(activityValue?.OriginalQuestion, isTestKnowledgeBase : false).ConfigureAwait(false); bool isSameQuestion = false; searchResult = qnaAnswerResponse.Answers.First(); // Check if question exist in the knowledgebase. if (searchResult != null && searchResult.Questions.Count > 0) { // Check if the deleted question & result returned from the knowledgebase are same. isSameQuestion = searchResult.Questions.First().ToUpperInvariant() == activityValue?.OriginalQuestion.ToUpperInvariant().Trim(); } // Delete the QnA pair if question exist in the knowledgebase & exactly the same question user wants to delete. if (searchResult.Id != -1 && isSameQuestion) { await qnaServiceProvider.DeleteQnaAsync(searchResult.Id.Value).ConfigureAwait(false); logger.LogInformation($"Question deleted by: {activity.Conversation.AadObjectId}"); attachment = MessagingExtensionQnaCard.DeletedEntry(activityValue?.OriginalQuestion, searchResult.Answer, activity.From.Name, activityValue?.UpdateHistoryData); ActivityEntity activityEntity = new ActivityEntity { ActivityReferenceId = searchResult.Metadata.FirstOrDefault(x => x.Name == Constants.MetadataActivityReferenceId)?.Value }; bool operationStatus = await activityStorageProvider.DeleteActivityEntityAsync(activityEntity).ConfigureAwait(false); if (!operationStatus) { logger.LogInformation($"Unable to delete the activity data from table storage."); } var updateCardActivity = new Activity(ActivityTypes.Message) { Id = turnContext.Activity.ReplyToId, Conversation = turnContext.Activity.Conversation, Attachments = new List <Attachment> { attachment }, }; // Send deleted question and answer card as response. await turnContext.UpdateActivityAsync(updateCardActivity, cancellationToken).ConfigureAwait(false); } else { // check if question and answer is present in unpublished version. qnaAnswerResponse = await qnaServiceProvider.GenerateAnswerAsync(activityValue?.OriginalQuestion, isTestKnowledgeBase : true).ConfigureAwait(false); if (qnaAnswerResponse?.Answers?.First().Id != -1) { await turnContext.SendActivityAsync(MessageFactory.Text(string.Format(CultureInfo.InvariantCulture, Strings.WaitMessage, activityValue?.OriginalQuestion))).ConfigureAwait(false); } } return; }
/// <summary> /// Validate the adaptive card fields while editing the question and answer pair. /// </summary> /// <param name="postedQnaPairEntity">Qna pair entity contains submitted card data.</param> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <returns>Envelope for Task Module Response.</returns> public async Task<TaskModuleResponse> EditQnAPairAsync( AdaptiveSubmitActionData postedQnaPairEntity, ITurnContext<IInvokeActivity> turnContext) { // Check if fields contains Html tags or Question and answer empty then return response with error message. if (Validators.IsContainsHtml(postedQnaPairEntity) || Validators.IsQnaFieldsNullOrEmpty(postedQnaPairEntity)) { // Returns the card with validation errors on add QnA task module. return await TaskModuleActivity.GetTaskModuleResponseAsync(MessagingExtensionQnaCard.AddQuestionForm(Validators.HtmlAndQnaEmptyValidation(postedQnaPairEntity), this.appBaseUri)).ConfigureAwait(false); } if (Validators.IsRichCard(postedQnaPairEntity)) { if (Validators.IsImageUrlInvalid(postedQnaPairEntity) || Validators.IsRedirectionUrlInvalid(postedQnaPairEntity)) { // Show the error message on task module response for edit QnA pair, if user has entered invalid image or redirection url. return await TaskModuleActivity.GetTaskModuleResponseAsync(MessagingExtensionQnaCard.AddQuestionForm(Validators.ValidateImageAndRedirectionUrls(postedQnaPairEntity), this.appBaseUri)).ConfigureAwait(false); } string combinedDescription = QnaHelper.BuildCombinedDescriptionAsync(postedQnaPairEntity); postedQnaPairEntity.IsRichCard = true; if (postedQnaPairEntity.UpdatedQuestion?.ToUpperInvariant().Trim() == postedQnaPairEntity.OriginalQuestion?.ToUpperInvariant().Trim()) { // Save the QnA pair, return the response and closes the task module. await TaskModuleActivity.GetTaskModuleResponseAsync(this.CardResponseAsync( turnContext, postedQnaPairEntity, combinedDescription).Result).ConfigureAwait(false); return default; } else { var hasQuestionExist = await this.qnaServiceProvider.QuestionExistsInKbAsync(postedQnaPairEntity.UpdatedQuestion).ConfigureAwait(false); if (hasQuestionExist) { // Shows the error message on task module, if question already exist. return await TaskModuleActivity.GetTaskModuleResponseAsync(this.CardResponseAsync( turnContext, postedQnaPairEntity, combinedDescription).Result).ConfigureAwait(false); } else { // Save the QnA pair, return the response and closes the task module. await TaskModuleActivity.GetTaskModuleResponseAsync(this.CardResponseAsync( turnContext, postedQnaPairEntity, combinedDescription).Result).ConfigureAwait(false); return default; } } } else { // Normal card section. if (postedQnaPairEntity.UpdatedQuestion?.ToUpperInvariant().Trim() == postedQnaPairEntity.OriginalQuestion?.ToUpperInvariant().Trim()) { // Save the QnA pair, return the response and closes the task module. await TaskModuleActivity.GetTaskModuleResponseAsync(this.CardResponseAsync( turnContext, postedQnaPairEntity, postedQnaPairEntity.Description).Result).ConfigureAwait(false); return default; } else { var hasQuestionExist = await this.qnaServiceProvider.QuestionExistsInKbAsync(postedQnaPairEntity.UpdatedQuestion).ConfigureAwait(false); if (hasQuestionExist) { // Shows the error message on task module, if question already exist. return await TaskModuleActivity.GetTaskModuleResponseAsync(this.CardResponseAsync( turnContext, postedQnaPairEntity, postedQnaPairEntity.Description).Result).ConfigureAwait(false); } else { // Save the QnA pair, return the response and closes the task module. await TaskModuleActivity.GetTaskModuleResponseAsync(this.CardResponseAsync( turnContext, postedQnaPairEntity, postedQnaPairEntity.Description).Result).ConfigureAwait(false); return default; } } } }
/// <summary> /// Get the reply to a question asked by end user. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="message">Text message.</param> /// <returns>A task that represents the work queued to execute.</returns> private async Task GetQuestionAnswerReplyAsync( ITurnContext <IMessageActivity> turnContext, IMessageActivity message) { string text = message.Text?.ToLower()?.Trim() ?? string.Empty; try { var queryResult = new QnASearchResultList(); ResponseCardPayload payload = new ResponseCardPayload(); if (!string.IsNullOrEmpty(message.ReplyToId) && (message.Value != null)) { payload = ((JObject)message.Value).ToObject <ResponseCardPayload>(); } queryResult = await this.qnaServiceProvider.GenerateAnswerAsync(question : text, isTestKnowledgeBase : false, payload.PreviousQuestions?.First().Id.ToString(), payload.PreviousQuestions?.First().Questions.First()).ConfigureAwait(false); if (queryResult.Answers.First().Id != -1) { var answerData = queryResult.Answers.First(); AnswerModel answerModel = new AnswerModel(); if (Validators.IsValidJSON(answerData.Answer)) { answerModel = JsonConvert.DeserializeObject <AnswerModel>(answerData.Answer); } if (!string.IsNullOrEmpty(answerModel?.Title) || !string.IsNullOrEmpty(answerModel?.Subtitle) || !string.IsNullOrEmpty(answerModel?.ImageUrl) || !string.IsNullOrEmpty(answerModel?.RedirectionUrl)) { await turnContext.SendActivityAsync(MessageFactory.Attachment(MessagingExtensionQnaCard.GetEndUserRichCard(text, answerData))).ConfigureAwait(false); } else { await turnContext.SendActivityAsync(MessageFactory.Attachment(ResponseCard.GetCard(answerData, text, this.appBaseUri, payload))).ConfigureAwait(false); } } else { await turnContext.SendActivityAsync(MessageFactory.Attachment(UnrecognizedInputCard.GetCard(text))).ConfigureAwait(false); } } catch (Exception ex) { // Check if knowledge base is empty and has not published yet when end user is asking a question to bot. if (((ErrorResponseException)ex).Response.StatusCode == HttpStatusCode.BadRequest) { var knowledgeBaseId = await this.configurationProvider.GetSavedEntityDetailAsync(Constants.KnowledgeBaseEntityId).ConfigureAwait(false); var hasPublished = await this.qnaServiceProvider.GetInitialPublishedStatusAsync(knowledgeBaseId).ConfigureAwait(false); // Check if knowledge base has not published yet. if (!hasPublished) { this.logger.LogError(ex, "Error while fetching the qna pair: knowledge base may be empty or it has not published yet."); await turnContext.SendActivityAsync(MessageFactory.Attachment(UnrecognizedInputCard.GetCard(text))).ConfigureAwait(false); return; } } // Throw the error at calling place, if there is any generic exception which is not caught. throw; } }