Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Commit

Permalink
Add FeedbackMiddleware to Solutions lib (#2226)
Browse files Browse the repository at this point in the history
* 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
lauren-mills committed Aug 30, 2019
1 parent 4d391d6 commit 6a17602
Show file tree
Hide file tree
Showing 13 changed files with 1,367 additions and 0 deletions.
67 changes: 67 additions & 0 deletions docs/_docs/howto/virtual-assistant/feedback.md
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/).
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);
}
}
}
Loading

0 comments on commit 6a17602

Please sign in to comment.