Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [#6865] Provide two new invoke handlers, (message/fetchTask, message/submitAction) for Custom Feedback Loops #6871

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext

case "task/submit":
return CreateInvokeResponse(await OnTeamsTaskModuleSubmitAsync(turnContext, SafeCast<TaskModuleRequest>(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false));

case "tab/fetch":
return CreateInvokeResponse(await OnTeamsTabFetchAsync(turnContext, SafeCast<TabRequest>(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false));

case "tab/submit":
return CreateInvokeResponse(await OnTeamsTabSubmitAsync(turnContext, SafeCast<TabSubmit>(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false));

Expand All @@ -98,6 +98,13 @@ protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext
case "config/submit":
return CreateInvokeResponse(await OnTeamsConfigSubmitAsync(turnContext, turnContext.Activity.Value as JObject, cancellationToken).ConfigureAwait(false));

case "message/submitAction":
await OnTeamsMessageSubmitActionAsync(turnContext, SafeCast<FeedbackResponse>(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false);
return CreateInvokeResponse();

case "message/fetchTask":
return CreateInvokeResponse(await OnTeamsMessageFetchTaskAsync(turnContext, cancellationToken).ConfigureAwait(false));

default:
return await base.OnInvokeActivityAsync(turnContext, cancellationToken).ConfigureAwait(false);
}
Expand Down Expand Up @@ -686,7 +693,7 @@ protected virtual Task OnTeamsChannelRenamedAsync(ChannelInfo channelInfo, TeamI
{
return Task.CompletedTask;
}

/// <summary>
/// Invoked when a Channel Restored event activity is received from the connector.
/// Channel Restored correspond to the user restoring a previously deleted channel.
Expand Down Expand Up @@ -994,6 +1001,69 @@ protected virtual Task OnTeamsMessageSoftDeleteAsync(ITurnContext<IMessageDelete
return Task.CompletedTask;
}

/// <summary>
/// Invoked when a feedback loop activity is received.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more accurate to say "Invoked when a user submits a default or custom feedback loop form".

/// </summary>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="feedback">A strongly-typed feedback loop object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
protected virtual Task OnTeamsMessageSubmitActionAsync(ITurnContext<IInvokeActivity> turnContext, FeedbackResponse feedback, CancellationToken cancellationToken)
{
throw new InvokeResponseException(HttpStatusCode.NotImplemented);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected to throw an error here?

}

/// <summary>
/// Invoked when a custom feedback loop activity is received.
/// The returned information is a <see cref="TaskModuleContinueResponse"/> instance,
/// that Teams will use to show either an AdaptiveCard or website in the "feedback window form".
/// <example>
/// <br/><br/>
/// <b>Example of a valid Adaptive Card payload:</b>
/// <code>
/// const taskModuleReturn = {
///   task: {
///     type: 'continue',
///     value: {
///       card: [Object], // Should contain valid Adaptive Card Payload
///       height: 200,
///       width: 400,
///       title: 'Test Task Module Title with AC'
///     }
///   }
/// };
/// return {
/// status: 200,
/// body: taskModuleReturn
/// };
/// </code>
/// <b>Example of a valid website payload:</b>
/// <code>
/// const taskModuleReturn = {
///   task: {
///     type: 'continue',
///     value: {
///       url: "https://bing.com", // Should contain valid URL with the valid domain listed under App Manifest
///     }
///   }
/// };
/// return {
/// status: 200,
/// body: taskModuleReturn
/// };
/// </code>
/// </example>
/// </summary>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents a <see cref="TaskModuleContinueResponse"/> instance, containing the necessary <see cref="TaskModuleTaskInfo"/> metadata with either an AdaptiveCard or a website url information.</returns>
protected virtual Task<TaskModuleContinueResponse> OnTeamsMessageFetchTaskAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
throw new InvokeResponseException(HttpStatusCode.NotImplemented);
}

/// <summary>
/// Safely casts an object to an object of type <typeparamref name="T"/> .
/// </summary>
Expand Down
42 changes: 42 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/FeedbackInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Bot.Schema.Teams
{
using Newtonsoft.Json;

/// <summary>
/// Describes feedback loop information.
/// </summary>
public partial class FeedbackInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="FeedbackInfo"/> class.
/// </summary>
public FeedbackInfo()
{
CustomInit();
}

/// <summary>
/// Initializes a new instance of the <see cref="FeedbackInfo"/> class.
/// </summary>
/// <param name="type">Unique identifier representing a team.</param>
public FeedbackInfo(string type = FeedbackInfoTypes.Default)
{
Type = type;
CustomInit();
}

/// <summary>
/// Gets or sets the feedback loop type. Possible values include: 'default', 'custom'.
/// </summary>
/// <value>
/// The feedback loop type (see <see cref="FeedbackInfoTypes"/>).
/// </value>
[JsonProperty(PropertyName = "type")]
public string Type { get; set; }

partial void CustomInit();
}
}
21 changes: 21 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/FeedbackInfoTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Bot.Schema.Teams
{
/// <summary>
/// Defines feedback loop type. Depending on the type, the feedback window will have a different structure.
/// </summary>
public static class FeedbackInfoTypes
{
/// <summary>
/// The type value for default feedback window form.
/// </summary>
public const string Default = "default";

/// <summary>
/// The type value for custom feedback window, can be either an AdaptiveCard or website.
/// </summary>
public const string Custom = "custom";
}
}
57 changes: 57 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/FeedbackResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Bot.Schema.Teams
{
using Newtonsoft.Json;

/// <summary>
/// Envelope for Feedback Response.
/// </summary>
public partial class FeedbackResponse
{
/// <summary>
/// Initializes a new instance of the <see cref="FeedbackResponse"/> class.
/// </summary>
public FeedbackResponse()
{
CustomInit();
}

/// <summary>
/// Initializes a new instance of the <see cref="FeedbackResponse"/> class.
/// </summary>
/// <param name="actionName">The name of the action.</param>
/// <param name="actionValue">The value of the action.</param>
/// <param name="replyToId">The ID of the message to which this message is a reply.</param>
public FeedbackResponse(string actionName = default, FeedbackResponseActionValue actionValue = default, string replyToId = default)
{
ActionName = actionName;
ActionValue = actionValue;
ReplyToId = replyToId;
CustomInit();
}

/// <summary>
/// Gets or sets the action name.
/// </summary>
/// <value>Name of the action.</value>
public string ActionName { get; set; } = "feedback";

/// <summary>
/// Gets or sets the response for the action value.
/// </summary>
/// <value>The action value that contains the feedback reaction and message.</value>
[JsonProperty(PropertyName = "actionValue")]
public FeedbackResponseActionValue ActionValue { get; set; }

/// <summary>
/// Gets or sets the ID of the message to which this message is a reply.
/// </summary>
/// <value>Value of the ID to reply.</value>
[JsonProperty(PropertyName = "replyToId")]
public string ReplyToId { get; set; }

partial void CustomInit();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Bot.Schema.Teams
{
using Newtonsoft.Json;

/// <summary>
/// Envelope for Feedback ActionValue Response.
/// </summary>
public partial class FeedbackResponseActionValue
{
/// <summary>
/// Initializes a new instance of the <see cref="FeedbackResponseActionValue"/> class.
/// </summary>
public FeedbackResponseActionValue()
{
CustomInit();
}

/// <summary>
/// Initializes a new instance of the <see cref="FeedbackResponseActionValue"/> class.
/// </summary>
/// <param name="reaction">The reaction of the feedback.</param>
/// <param name="feedback">The feedback content.</param>
public FeedbackResponseActionValue(string reaction = default, string feedback = default)
{
Reaction = reaction;
Feedback = feedback;
CustomInit();
}

/// <summary>
/// Gets or sets the reaction, either "like" or "dislike".
/// </summary>
/// <value>val.</value>
[JsonProperty(PropertyName = "reaction")]
public string Reaction { get; set; }

/// <summary>
/// Gets or sets the feedback content provided by the user when prompted with "What did you like/dislike?".
/// </summary>
/// <value>val.</value>
[JsonProperty(PropertyName = "feedback")]
public string Feedback { get; set; }

partial void CustomInit();
}
}
7 changes: 7 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/TeamsChannelData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ public TeamsChannelData(ChannelInfo channel = default, string eventType = defaul
[JsonProperty(PropertyName = "settings")]
public TeamsChannelDataSettings Settings { get; set; }

/// <summary>
/// Gets or sets information about custom feedback buttons.
/// </summary>
/// <value>The <see cref="FeedbackInfo"/> for this <see cref="TeamsChannelData"/>.</value>
[JsonProperty(PropertyName = "feedbackLoop")]
public FeedbackInfo FeedbackLoop { get; set; }

/// <summary>
/// Gets the OnBehalfOf list for user attribution.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,64 @@ void CaptureSend(Activity[] arg)
Assert.Equal(200, ((InvokeResponse)activitiesToSend[0].Value).Status);
}

[Fact]
public async Task TestMessageSubmitAction()
{
// Arrange
var activity = new Activity
{
Type = ActivityTypes.Invoke,
Name = "message/submitAction"
};

Activity[] activitiesToSend = null;
void CaptureSend(Activity[] arg)
{
activitiesToSend = arg;
}

var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity);

// Act
var bot = new TestActivityHandler();
await ((IBot)bot).OnTurnAsync(turnContext);

// Assert
Assert.NotNull(activitiesToSend);
Assert.Single(activitiesToSend);
Assert.IsType<InvokeResponse>(activitiesToSend[0].Value);
Assert.Equal(200, ((InvokeResponse)activitiesToSend[0].Value).Status);
}

[Fact]
public async Task TestMessageFetchTask()
{
// Arrange
var activity = new Activity
{
Type = ActivityTypes.Invoke,
Name = "message/fetchTask"
};

Activity[] activitiesToSend = null;
void CaptureSend(Activity[] arg)
{
activitiesToSend = arg;
}

var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity);

// Act
var bot = new TestActivityHandler();
await ((IBot)bot).OnTurnAsync(turnContext);

// Assert
Assert.NotNull(activitiesToSend);
Assert.Single(activitiesToSend);
Assert.IsType<InvokeResponse>(activitiesToSend[0].Value);
Assert.Equal(501, ((InvokeResponse)activitiesToSend[0].Value).Status);
}

private class TestActivityHandler : TeamsActivityHandler
{
}
Expand Down
Loading
Loading