Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
53 changes: 51 additions & 2 deletions Libraries/Microsoft.Teams.Api/Activities/Activity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,31 @@ public virtual Activity WithData(ChannelData value)
{
ChannelData ??= new();
ChannelData.Merge(value);
NormalizeFeedback();
Comment thread
lilyydu marked this conversation as resolved.
return this;
}

/// <summary>
/// The Teams service rejects <c>feedbackLoop</c> and <c>feedbackLoopEnabled</c>
/// set at the same time. When <see cref="ChannelData.FeedbackLoop"/> is set it
/// wins; otherwise a legacy <c>FeedbackLoopEnabled = true</c> is upgraded to
/// <see cref="FeedbackType.Default"/>.
/// </summary>
private void NormalizeFeedback()
{
if (ChannelData is null) return;

if (ChannelData.FeedbackLoop is not null)
{
ChannelData.FeedbackLoopEnabled = null;
}
else if (ChannelData.FeedbackLoopEnabled == true)
{
ChannelData.FeedbackLoop = new FeedbackLoop(FeedbackType.Default);
ChannelData.FeedbackLoopEnabled = null;
}
Comment thread
lilyydu marked this conversation as resolved.
}

public virtual Activity WithData(string key, object? value)
{
ChannelData ??= new();
Expand Down Expand Up @@ -373,12 +395,39 @@ public virtual Activity AddSensitivityLabel(string name, string? description = n
}

/// <summary>
/// enable/disable message feedback
/// Legacy builder method of enabling default message feedback.
/// </summary>
/// <param name="value">Whether to enable default message feedback.</param>
public virtual Activity AddFeedback(bool value = true)
{
ChannelData ??= new();
ChannelData.FeedbackLoopEnabled = value;

if (value)
{
ChannelData.FeedbackLoop = new FeedbackLoop(FeedbackType.Default);
}
else
{
ChannelData.FeedbackLoop = null;
}

ChannelData.FeedbackLoopEnabled = null;
return this;
}

/// <summary>
/// Enable message feedback with an explicit mode (default or custom).
/// </summary>
/// <param name="mode">
/// <see cref="FeedbackType.Default"/> shows Teams' built-in thumbs up/down UI.
/// <see cref="FeedbackType.Custom"/> triggers a <c>message/fetchTask</c> invoke
/// so the bot can return its own task module dialog.
/// </param>
public virtual Activity AddFeedback(FeedbackType mode)
{
ChannelData ??= new();
ChannelData.FeedbackLoop = new FeedbackLoop(mode);
ChannelData.FeedbackLoopEnabled = null;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ public partial class Name : StringEnum
public abstract class MessageActivity(Name.Messages name) : InvokeActivity(new(name.Value))
{
public Messages.SubmitActionActivity ToSubmitAction() => (Messages.SubmitActionActivity)this;
public Messages.FetchTaskActivity ToFetchTask() => (Messages.FetchTaskActivity)this;

public override object ToType(Type type, IFormatProvider? provider)
{
if (type == typeof(Messages.SubmitActionActivity)) return ToSubmitAction();
if (type == typeof(Messages.FetchTaskActivity)) return ToFetchTask();
return this;
}

Expand Down Expand Up @@ -53,6 +55,7 @@ public override bool CanConvert(Type typeToConvert)
return name switch
{
"message/submitAction" => JsonSerializer.Deserialize<Messages.SubmitActionActivity>(element.ToString(), options),
"message/fetchTask" => JsonSerializer.Deserialize<Messages.FetchTaskActivity>(element.ToString(), options),
_ => throw new JsonException($"failed to deserialize message activity '{name}' doesn't match any known types.")
};
}
Expand All @@ -65,6 +68,12 @@ public override void Write(Utf8JsonWriter writer, MessageActivity value, JsonSer
return;
}

if (value is Messages.FetchTaskActivity fetchTask)
{
JsonSerializer.Serialize(writer, fetchTask, options);
return;
}

JsonSerializer.Serialize(writer, value, options);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

using Microsoft.Teams.Common;

namespace Microsoft.Teams.Api.Activities.Invokes;

public partial class Name : StringEnum
{
public partial class Messages : StringEnum
{
public static readonly Messages FetchTask = new("message/fetchTask");
public bool IsFetchTask => FetchTask.Equals(Value);
}
}

/// <summary>
/// The feedback button the user clicked.
/// </summary>
[JsonConverter(typeof(JsonConverter<Reaction>))]
public partial class Reaction(string value) : StringEnum(value)
{
public static readonly Reaction Like = new("like");
public bool IsLike => Like.Equals(Value);

public static readonly Reaction Dislike = new("dislike");
public bool IsDislike => Dislike.Equals(Value);
}

public static partial class Messages
{
/// <summary>
/// Sent when a message has a custom feedback loop and the user clicks a
/// feedback button. The bot should respond with a task module (dialog) to
/// collect feedback.
/// </summary>
public class FetchTaskActivity() : MessageActivity(Name.Messages.FetchTask)
{
/// <summary>
/// A value that is associated with the activity.
/// </summary>
[JsonPropertyName("value")]
[JsonPropertyOrder(32)]
public new required FetchTaskValue Value
{
get => (FetchTaskValue)base.Value!;
set => base.Value = value;
}

/// <summary>
/// The value associated with a message fetch task.
/// </summary>
public class FetchTaskValue
{
/// <summary>
/// The data payload containing action name and value.
/// </summary>
[JsonPropertyName("data")]
[JsonPropertyOrder(0)]
public required FetchTaskData Data { get; set; }
}

/// <summary>
/// The data payload nested inside the fetch task value.
/// </summary>
public class FetchTaskData
{
/// <summary>
/// The name of the action.
/// </summary>
[JsonPropertyName("actionName")]
[JsonPropertyOrder(0)]
public string ActionName { get; set; } = "feedback";

/// <summary>
/// Contains the user's reaction.
/// </summary>
[JsonPropertyName("actionValue")]
[JsonPropertyOrder(1)]
public required FetchTaskActionValue ActionValue { get; set; }
}

/// <summary>
/// The nested action value containing the user's reaction.
/// </summary>
public class FetchTaskActionValue
{
/// <summary>
/// The feedback button the user clicked.
/// </summary>
[JsonPropertyName("reaction")]
[JsonPropertyOrder(0)]
public required Reaction Reaction { get; set; }
}
}
}
16 changes: 15 additions & 1 deletion Libraries/Microsoft.Teams.Api/ChannelData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ public class ChannelData
public App? App { get; set; }

/// <summary>
/// Whether or not the feedback loop feature is enabled
/// Legacy feedback loop flag. Setting this to <c>true</c> is equivalent to
/// <c>FeedbackLoop = new FeedbackLoop(FeedbackType.Default)</c>.
/// Prefer setting <see cref="FeedbackLoop"/> directly; this field is normalized
/// by <see cref="Activities.Activity.WithData(ChannelData)"/>.
/// </summary>
[JsonPropertyName("feedbackLoopEnabled")]
[JsonPropertyOrder(7)]
Expand Down Expand Up @@ -109,6 +112,17 @@ public class ChannelData
[JsonPropertyOrder(14)]
public MembershipSource? MembershipSource { get; set; }

/// <summary>
/// Feedback loop configuration.
/// Set <c>Type</c> to <see cref="FeedbackType.Custom"/> to trigger a
/// <c>message/fetchTask</c> invoke for a bot-provided task module dialog.
/// Set <c>Type</c> to <see cref="FeedbackType.Default"/> for the standard
/// Teams thumbs up/down UI.
/// </summary>
[JsonPropertyName("feedbackLoop")]
[JsonPropertyOrder(15)]
public FeedbackLoop? FeedbackLoop { get; set; }

/// <summary>
/// All extra data present
/// </summary>
Expand Down
44 changes: 44 additions & 0 deletions Libraries/Microsoft.Teams.Api/FeedbackLoop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

using Microsoft.Teams.Common;

namespace Microsoft.Teams.Api;

/// <summary>
/// The type of feedback loop.
/// Use <c>Custom</c> to trigger a <c>message/fetchTask</c> invoke so the bot
/// can return its own task module dialog.
/// Use <c>Default</c> for the standard Teams thumbs up/down UI.
/// </summary>
[JsonConverter(typeof(JsonConverter<FeedbackType>))]
public partial class FeedbackType(string value) : StringEnum(value)
{
public static readonly FeedbackType Default = new("default");
public bool IsDefault => Default.Equals(Value);

public static readonly FeedbackType Custom = new("custom");
public bool IsCustom => Custom.Equals(Value);
}

/// <summary>
/// Configuration for a feedback loop on a message.
/// </summary>
public class FeedbackLoop
{
/// <summary>
/// The type of feedback loop.
/// </summary>
[JsonPropertyName("type")]
[JsonPropertyOrder(0)]
public FeedbackType Type { get; set; } = FeedbackType.Default;

public FeedbackLoop() { }

public FeedbackLoop(FeedbackType type)
{
Type = type;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Teams.Api.Activities;
using Microsoft.Teams.Api.Activities.Invokes;
using Microsoft.Teams.Apps.Routing;

namespace Microsoft.Teams.Apps.Activities.Invokes;

public static partial class Message
{
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class FetchTaskAttribute() : InvokeAttribute(Api.Activities.Invokes.Name.Messages.FetchTask, typeof(Messages.FetchTaskActivity))
{
public override object Coerce(IContext<IActivity> context) => context.ToActivityType<Messages.FetchTaskActivity>();
}
}

public static partial class AppInvokeActivityExtensions
{
/// <summary>
/// Registers a handler for <c>message/fetchTask</c> activities.
/// The bot should return a task module response containing the dialog to show the user.
/// </summary>
public static App OnMessageFetchTask(this App app, Func<IContext<Messages.FetchTaskActivity>, CancellationToken, Task<Api.TaskModules.Response>> handler)
{
app.Router.Register(new Route()
{
Name = string.Join("/", [ActivityType.Invoke, Name.Messages.FetchTask]),
Type = app.Status is null ? RouteType.System : RouteType.User,
Handler = async context => await handler(context.ToActivityType<Messages.FetchTaskActivity>(), context.CancellationToken),
Comment thread
lilyydu marked this conversation as resolved.
Selector = activity => activity is Messages.FetchTaskActivity
});

return app;
}
}
45 changes: 43 additions & 2 deletions Samples/Samples.AI/Handlers/FeedbackHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
using Microsoft.Teams.AI.Models.OpenAI;
using Microsoft.Teams.AI.Prompts;
using Microsoft.Teams.AI.Templates;
using Microsoft.Teams.Api;
using Microsoft.Teams.Api.Activities.Invokes;
using Microsoft.Teams.Apps;
using Microsoft.Teams.Cards;

using TaskModules = Microsoft.Teams.Api.TaskModules;

namespace Samples.AI.Handlers;

Expand Down Expand Up @@ -38,13 +42,15 @@ public static async Task HandleFeedbackLoop(OpenAIChatModel model, IContext<Micr
{
context.Log.Info($"[HANDLER] AI response received: {result.Content}");

// Create message with AI generated indicator and feedback buttons
// Create message with AI generated indicator and custom feedback buttons.
// Clicking a feedback button in 'custom' mode triggers a message/fetchTask
// invoke (handled by HandleFeedbackFetchTask) so the bot can show its own dialog.
var messageActivity = new Microsoft.Teams.Api.Activities.MessageActivity
{
Text = result.Content,
}
.AddAIGenerated()
.AddFeedback(); // This adds the thumbs up/down buttons
.AddFeedback(FeedbackType.Custom);

context.Log.Info("[HANDLER] Sending message with feedback buttons");
var sentActivity = await context.Send(messageActivity, cancellationToken);
Expand All @@ -70,6 +76,41 @@ public static async Task HandleFeedbackLoop(OpenAIChatModel model, IContext<Micr
}
}

/// <summary>
/// Builds the task module (dialog) shown when the user clicks a feedback
/// button on a message whose feedback loop type is <c>custom</c>.
/// </summary>
public static TaskModules.Response HandleFeedbackFetchTask(IContext<Messages.FetchTaskActivity> context)
{
var reaction = context.Activity.Value.Data.ActionValue.Reaction;
context.Log.Info($"[HANDLER] Feedback fetch-task invoked, reaction: {reaction}");

var card = new AdaptiveCard
{
Schema = "http://adaptivecards.io/schemas/adaptive-card.json",
Body = new List<CardElement>
{
new TextBlock($"You reacted {reaction}. Tell us more (optional):") { Wrap = true },
new TextInput
{
Id = "feedbackText",
Placeholder = "Your feedback...",
IsMultiline = true,
}
},
Actions = new List<Microsoft.Teams.Cards.Action>
{
new SubmitAction { Title = "Submit" }
}
};

return new TaskModules.Response(new TaskModules.ContinueTask(new TaskModules.TaskInfo
{
Title = "Feedback",
Card = new Attachment(card),
Comment thread
lilyydu marked this conversation as resolved.
}));
}

/// <summary>
/// Handles feedback submissions from users
/// </summary>
Expand Down
Loading
Loading