This repository has been archived by the owner on Jun 30, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 528
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add FeedbackMiddleware to Solutions lib (#2226)
* Adding feedback middleware and classes to solutions lib * Add localization * stylecop * updated middleware to log more consistent values * added feedback documentation * Added pbit with feedback dashboard * Update feedback.md
- Loading branch information
1 parent
4d391d6
commit 6a17602
Showing
13 changed files
with
1,367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
--- | ||
category: How To | ||
subcategory: Virtual Assistant | ||
title: Collect Feedback | ||
description: Describes how to implement the FeedbackMiddleware to collect user feedback. | ||
order: 6 | ||
--- | ||
|
||
# {{ page.title }} | ||
{:.no_toc} | ||
|
||
## In this how-to | ||
{:.no_toc} | ||
|
||
* | ||
{:toc} | ||
|
||
## Add and configure the middleware | ||
To start collecting user feedback, add the following code block in your adapter class (DefaultAdapter.cs in the Virtual Assistant and Skill templates): | ||
|
||
```csharp | ||
Use(new FeedbackMiddleware(conversationState, telemetryClient, new FeedbackOptions())); | ||
``` | ||
|
||
This enables the FeedbackMiddleware with the following default settings: | ||
|
||
| Property | Description | Type | Default value | | ||
| -------- | ----------- | ---- |------------- | | ||
| FeedbackActions | Feedback options shown to the user. | `List<CardAction>` | 👍 / 👎 | | ||
| DismissAction | Option to dismiss request for feedback, or request for comment. | `CardAction` | *Dismiss* | ||
| FeedbackReceivedMessage | Message to show after user has provided feedback. | `string` | *Thanks for your feedback!* | | ||
| CommentsEnabled | Flag indicating whether the bot should prompt for free-form comments after user has provided feedback. | `bool` | false | | ||
| CommentPrompt | Message to show after user provided feedback if CommentsEnabled is true. | `string` | *Please add any additional comments in the chat.* | ||
| CommentReceivedMessage | Message to show after user provides a free-form comment. | `string` | *Your comment has been received.* | | ||
|
||
Here is an example customization with different feedback options and comments enabled: | ||
|
||
```csharp | ||
Use(new FeedbackMiddleware(conversationState, telemetryClient, new FeedbackOptions() | ||
{ | ||
FeedbackActions = new List<CardAction>() | ||
{ | ||
new CardAction(ActionTypes.PostBack, title: "🙂", value: "positive"), | ||
new CardAction(ActionTypes.PostBack, title: "😐", value: "neutral"), | ||
new CardAction(ActionTypes.PostBack, title: "🙁", value: "negative"), | ||
}; | ||
CommentsEnabled = true | ||
})); | ||
``` | ||
|
||
## Request feedback | ||
You can request feedback from your users using the following code snippet: | ||
|
||
```csharp | ||
FeedbackMiddleware.RequestFeedbackAsync(turnContext, "your-tag") | ||
``` | ||
> Replace "your-tag" with a custom label for your feedback to be shown in Power BI dashboard. For example, QnA Maker feedback might be labelled "qna". | ||
## Request feedback in skills | ||
To enable requesting feedback in your skills, you must either be using the same state storage and Application Insights services as your Virtual Assistant (with FeedbackMiddleware enabled) or you need to follow the above steps to configure the FeedbackMiddleware in your adapter. | ||
|
||
After the middleware is configured, you can request feedback as usual. | ||
|
||
## View your feedback in Power BI | ||
You can view your **Feedback** in the Feedback tab of the Conversational AI Dashboard. | ||
|
||
More information on Power BI and Analytics in Virtual Assistant can be found [here]({{site.repo}}/reference/analytics/powerbi/). |
214 changes: 214 additions & 0 deletions
214
...soft.bot.builder.solutions/microsoft.bot.builder.solutions/Feedback/FeedbackMiddleware.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Bot.Builder.Dialogs.Choices; | ||
using Microsoft.Bot.Schema; | ||
|
||
namespace Microsoft.Bot.Builder.Solutions.Feedback | ||
{ | ||
public class FeedbackMiddleware : IMiddleware | ||
{ | ||
private static FeedbackOptions _options; | ||
private static IStatePropertyAccessor<FeedbackRecord> _feedbackAccessor; | ||
private static ConversationState _conversationState; | ||
private IBotTelemetryClient _telemetryClient; | ||
private string _traceName = "Feedback"; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="FeedbackMiddleware"/> class. | ||
/// </summary> | ||
/// <param name="conversationState">The conversation state used for storing the feedback record before logging to Application Insights.</param> | ||
/// <param name="telemetryClient">The bot telemetry client used for logging the feedback record in Application Insights.</param> | ||
/// <param name="options">(Optional ) Feedback options object configuring the feedback actions and responses.</param> | ||
public FeedbackMiddleware( | ||
ConversationState conversationState, | ||
IBotTelemetryClient telemetryClient, | ||
FeedbackOptions options = null) | ||
{ | ||
_conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState)); | ||
_telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); | ||
_options = options ?? new FeedbackOptions(); | ||
|
||
// Create FeedbackRecord state accessor | ||
_feedbackAccessor = conversationState.CreateProperty<FeedbackRecord>(nameof(FeedbackRecord)); | ||
} | ||
|
||
/// <summary> | ||
/// Sends a Feedback Request activity with suggested actions to the user. | ||
/// </summary> | ||
/// <param name="context">Turn context for sending activities.</param> | ||
/// <param name="tag">Tag to categorize feedback record in Application Insights.</param> | ||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | ||
public static async Task RequestFeedbackAsync(ITurnContext context, string tag) | ||
{ | ||
// clear state | ||
await _feedbackAccessor.DeleteAsync(context).ConfigureAwait(false); | ||
|
||
// create feedbackRecord with original activity and tag | ||
var record = new FeedbackRecord() | ||
{ | ||
Request = context.Activity, | ||
Tag = tag, | ||
}; | ||
|
||
// store in state. No need to save changes, because its handled in IBot | ||
await _feedbackAccessor.SetAsync(context, record).ConfigureAwait(false); | ||
|
||
// If channel supports suggested actions | ||
if (Channel.SupportsSuggestedActions(context.Activity.ChannelId)) | ||
{ | ||
// prompt for feedback | ||
// if activity already had suggested actions, add the feedback actions | ||
if (context.Activity.SuggestedActions != null) | ||
{ | ||
var actions = new List<CardAction>() | ||
.Concat(context.Activity.SuggestedActions.Actions) | ||
.Concat(GetFeedbackActions()) | ||
.ToList(); | ||
|
||
await context.SendActivityAsync(MessageFactory.SuggestedActions(actions)).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
var actions = GetFeedbackActions(); | ||
await context.SendActivityAsync(MessageFactory.SuggestedActions(actions)).ConfigureAwait(false); | ||
} | ||
} | ||
else | ||
{ | ||
// else channel doesn't support suggested actions, so use hero card. | ||
var hero = new HeroCard(buttons: GetFeedbackActions()); | ||
await context.SendActivityAsync(MessageFactory.Attachment(hero.ToAttachment())).ConfigureAwait(false); | ||
} | ||
} | ||
|
||
public async Task OnTurnAsync(ITurnContext context, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken)) | ||
{ | ||
// get feedback record from state. If we don't find anything, set to null. | ||
var record = await _feedbackAccessor.GetAsync(context, () => null).ConfigureAwait(false); | ||
|
||
// if we have requested feedback | ||
if (record != null) | ||
{ | ||
if (_options.FeedbackActions.Any(f => context.Activity.Text == (string)f.Value || context.Activity.Text == f.Title)) | ||
{ | ||
// if activity text matches a feedback action | ||
// save feedback in state | ||
var feedback = _options.FeedbackActions | ||
.Where(f => context.Activity.Text == (string)f.Value || context.Activity.Text == f.Title) | ||
.First(); | ||
|
||
// Set the feedback to the action value for consistency | ||
record.Feedback = (string)feedback.Value; | ||
await _feedbackAccessor.SetAsync(context, record).ConfigureAwait(false); | ||
|
||
if (_options.CommentsEnabled) | ||
{ | ||
// if comments are enabled | ||
// create comment prompt with dismiss action | ||
if (Channel.SupportsSuggestedActions(context.Activity.ChannelId)) | ||
{ | ||
var commentPrompt = MessageFactory.SuggestedActions( | ||
text: $"{_options.FeedbackReceivedMessage} {_options.CommentPrompt}", | ||
cardActions: new List<CardAction>() { _options.DismissAction }); | ||
|
||
// prompt for comment | ||
await context.SendActivityAsync(commentPrompt).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
// channel doesn't support suggestedActions, so use hero card. | ||
var hero = new HeroCard( | ||
text: _options.CommentPrompt, | ||
buttons: new List<CardAction> { _options.DismissAction }); | ||
|
||
// prompt for comment | ||
await context.SendActivityAsync(MessageFactory.Attachment(hero.ToAttachment())).ConfigureAwait(false); | ||
} | ||
} | ||
else | ||
{ | ||
// comments not enabled, respond and cleanup | ||
// send feedback response | ||
await context.SendActivityAsync(_options.FeedbackReceivedMessage).ConfigureAwait(false); | ||
|
||
// log feedback in appInsights | ||
LogFeedback(record); | ||
|
||
// clear state | ||
await _feedbackAccessor.DeleteAsync(context).ConfigureAwait(false); | ||
} | ||
} | ||
else if (context.Activity.Text == (string)_options.DismissAction.Value || context.Activity.Text == _options.DismissAction.Title) | ||
{ | ||
// if user dismissed | ||
// log existing feedback | ||
if (!string.IsNullOrEmpty(record.Feedback)) | ||
{ | ||
// log feedback in appInsights | ||
LogFeedback(record); | ||
} | ||
|
||
// clear state | ||
await _feedbackAccessor.DeleteAsync(context).ConfigureAwait(false); | ||
} | ||
else if (!string.IsNullOrEmpty(record.Feedback) && _options.CommentsEnabled) | ||
{ | ||
// if we received a comment and user didn't dismiss | ||
// store comment in state | ||
record.Comment = context.Activity.Text; | ||
await _feedbackAccessor.SetAsync(context, record).ConfigureAwait(false); | ||
|
||
// Respond to comment | ||
await context.SendActivityAsync(_options.CommentReceivedMessage).ConfigureAwait(false); | ||
|
||
// log feedback in appInsights | ||
LogFeedback(record); | ||
|
||
// clear state | ||
await _feedbackAccessor.DeleteAsync(context).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
// we requested feedback, but the user responded with something else | ||
// clear state and continue (so message can be handled by dialog stack) | ||
await _feedbackAccessor.DeleteAsync(context).ConfigureAwait(false); | ||
await next(cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
await _conversationState.SaveChangesAsync(context).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
// We are not requesting feedback. Go to next. | ||
await next(cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
|
||
private static List<CardAction> GetFeedbackActions() | ||
{ | ||
var actions = new List<CardAction>(_options.FeedbackActions) | ||
{ | ||
_options.DismissAction, | ||
}; | ||
return actions; | ||
} | ||
|
||
private void LogFeedback(FeedbackRecord record) | ||
{ | ||
var properties = new Dictionary<string, string>() | ||
{ | ||
{ nameof(FeedbackRecord.Tag), record.Tag }, | ||
{ nameof(FeedbackRecord.Feedback), record.Feedback }, | ||
{ nameof(FeedbackRecord.Comment), record.Comment }, | ||
{ nameof(FeedbackRecord.Request.Text), record.Request?.Text }, | ||
{ nameof(FeedbackRecord.Request.Id), record.Request?.Conversation.Id }, | ||
{ nameof(FeedbackRecord.Request.ChannelId), record.Request?.ChannelId }, | ||
}; | ||
|
||
_telemetryClient.TrackEvent(_traceName, properties); | ||
} | ||
} | ||
} |
Oops, something went wrong.