diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AI.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AI.cs index f7c74c3e6..3b9f1b3c2 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AI.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AI.cs @@ -40,7 +40,7 @@ public AI(AIOptions options, ILoggerFactory? loggerFactory = null) _actions = new ActionCollection(); // Import default actions - ImportActions(new DefaultActions(loggerFactory)); + ImportActions(new DefaultActions(options.EnableFeedbackLoop, loggerFactory)); } /// diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AIOptions.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AIOptions.cs index 4c2e2b3f5..4017df5eb 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AIOptions.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/AIOptions.cs @@ -56,6 +56,12 @@ public sealed class AIOptions where TState : TurnState /// public bool? AllowLooping { get; set; } + /// + /// Optional. If true, the AI system will enable the feedback loop in Teams that allows a user to give thumbs up or down to a response. + /// Defaults to "false". + /// + public bool EnableFeedbackLoop { get; set; } = false; + /// /// Initializes a new instance of the class. /// diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/AIEntity.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/AIEntity.cs new file mode 100644 index 000000000..0976cb1c0 --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/AIEntity.cs @@ -0,0 +1,204 @@ +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Teams.AI.AI.Action +{ + /// + /// The citations's AIEntity. + /// + public class AIEntity : Entity + { + /// + /// Required. Must be "https://schema.org/Message" + /// + [JsonProperty(PropertyName = "type")] + public new string Type = "https://schema.org/Message"; + + /// + /// Required. Must be "Message". + /// + [JsonProperty(PropertyName = "@type")] + public string AtType = "Message"; + + /// + /// Required. Must be "https://schema.org" + /// + [JsonProperty(PropertyName = "@context")] + public string AtContext = "https://schema.org"; + + /// + /// Must be left blank. This is for Bot Framework's schema. + /// + [JsonProperty(PropertyName = "@id")] + public string AtId = ""; + + /// + /// Indicate that the content was generated by AI. + /// + [JsonProperty(PropertyName = "additionalType")] + public List AdditionalType = new() { "AIGeneratedContent" }; + + /// + /// Optional. If the citation object is included, then the sent activity will include citations that are referenced in the activity text. + /// + [JsonProperty(PropertyName = "citation")] + public List Citation { get; set; } = new(); + } + + /// + /// The client citation. + /// + public class ClientCitation + { + /// + /// Required. Must be "Claim". + /// + [JsonProperty(PropertyName = "@type")] + public string AtType = "Claim"; + + /// + /// Required. Number and position of the citation. + /// + [JsonProperty(PropertyName = "position")] + public string Position { get; set; } = string.Empty; + + /// + /// The citation's appearance. + /// + [JsonProperty(PropertyName = "appearance")] + public ClientCitationAppearance? Appearance { get; set; } + + } + + /// + /// The client citation appearance. + /// + public class ClientCitationAppearance + { + /// + /// Required. Must be "DigitalDocument" + /// + [JsonProperty(PropertyName = "@type")] + public string AtType = "DigitalDocument"; + + /// + /// Name of the document. + /// + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } = string.Empty; + + /// + /// Optional. The citation appreance text. It is ignored in Teams. + /// + [JsonProperty(PropertyName = "text")] + public string? Text { get; set; } + + /// + /// URL of the document. This will make the name of the citation clickable and direct the user to the specified URL. + /// + [JsonProperty(PropertyName = "url")] + public string? Url { get; set; } + + /// + /// Content of the citation. Should be clipped if longer than ~500 characters. + /// + [JsonProperty(PropertyName = "abstract")] + public string Abstract { get; set; } = string.Empty; + + /// + /// The encoding format used for the icon. + /// + [JsonProperty(PropertyName = "encodingFormat")] + public string EncodingFormat { get; set; } = "text/html"; + + /// + /// The icon provided in the citation ui. + /// + [JsonProperty(PropertyName = "image")] + public string? Image { get; set; } + + /// + /// Optional. Set the keywords. + /// + [JsonProperty(PropertyName = "keywords")] + public List? Keywords { get; set; } + + /// + /// Optional sensitivity content information. + /// + [JsonProperty(PropertyName = "usageInfo")] + public SensitivityUsageInfo? UsageInfo { get; set; } + } + + /// + /// The sensitivity usage info. + /// + public class SensitivityUsageInfo + { + /// + /// Must be "https://schema.org/Message" + /// + [JsonProperty(PropertyName = "type")] + public string Type = "https://schema.org/Message"; + + /// + /// Required. Set to "CreativeWork". + /// + [JsonProperty(PropertyName = "@type")] + public string AtType = "CreativeWork"; + + /// + /// Sensitivity description of the content. + /// + [JsonProperty(PropertyName = "description")] + public string? Description { get; set; } + + /// + /// Sensitivity title of the content. + /// + [JsonProperty(PropertyName = "name")] + public string? Name { get; set; } + + /// + /// Optional. Ignored in Teams + /// + [JsonProperty(PropertyName = "position")] + public int Position { get; set; } + + /// + /// The sensitivity usage info pattern. + /// + [JsonProperty(PropertyName = "pattern")] + public SensitivityUsageInfoPattern? Pattern; + } + + /// + /// The sensitivity usage info pattern. + /// + public class SensitivityUsageInfoPattern + { + /// + /// Set to "DefinedTerm". + /// + [JsonProperty(PropertyName = "@type")] + public string AtType = "DefinedTerm"; + + /// + /// Whether it's in a defined term set. + /// + [JsonProperty(PropertyName = "inDefinedTermSet")] + public string? inDefinedTermSet { get; set; } + + /// + /// The color. + /// + [JsonProperty(PropertyName = "name")] + public string? Name { get; set; } + + /// + /// For example `#454545`. + /// + [JsonProperty(PropertyName = "termCode")] + public string? TermCode { get; set; } + } +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/DefaultActions.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/DefaultActions.cs index 3386231e4..0fd13639a 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/DefaultActions.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Action/DefaultActions.cs @@ -6,15 +6,19 @@ using Microsoft.Extensions.Logging; using Microsoft.Bot.Builder; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Teams.AI.AI.Models; +using Microsoft.Bot.Schema; namespace Microsoft.Teams.AI.AI.Action { internal class DefaultActions where TState : TurnState { private readonly ILogger _logger; + private readonly bool _enableFeedbackLoop; - public DefaultActions(ILoggerFactory? loggerFactory = null) + public DefaultActions(bool enableFeedbackLoop = false, ILoggerFactory? loggerFactory = null) { + _enableFeedbackLoop = enableFeedbackLoop; _logger = loggerFactory is null ? NullLogger.Instance : loggerFactory.CreateLogger(typeof(DefaultActions)); } @@ -79,14 +83,71 @@ public async Task SayCommandAsync([ActionTurnContext] ITurnContext turnC Verify.ParamNotNull(command); Verify.ParamNotNull(command.Response); - if (turnContext.Activity.ChannelId == Channels.Msteams) + if (command.Response.Content == null || command.Response.Content == string.Empty) { - await turnContext.SendActivityAsync(command.Response.Content.Replace("\n", "
"), null, null, cancellationToken); + return ""; } - else + + string content = command.Response.Content; + + bool isTeamsChannel = turnContext.Activity.ChannelId == Channels.Msteams; + + if (isTeamsChannel) + { + content.Replace("\n", "
"); + } + + // If the response from the AI includes citations, those citations will be parsed and added to the SAY command. + List citations = new(); + + if (command.Response.Context != null && command.Response.Context.Citations.Count > 0) + { + int i = 0; + foreach (Citation citation in command.Response.Context.Citations) + { + string abs = CitationUtils.Snippet(citation.Content, 500); + if (isTeamsChannel) + { + content.Replace("\n", "
"); + }; + + citations.Add(new ClientCitation() + { + Position = $"{i + 1}", + Appearance = new ClientCitationAppearance() + { + Name = citation.Title, + Abstract = abs + } + }); + i++; + } + } + + // If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc. + string contentText = citations.Count == 0 ? content : CitationUtils.FormatCitationsResponse(content); + + // If there are citations, filter out the citations unused in content. + List? referencedCitations = citations.Count > 0 ? CitationUtils.GetUsedCitations(contentText, citations) : new List(); + + object? channelData = isTeamsChannel ? new + { + feedbackLoopEnabled = _enableFeedbackLoop + } : null; + + AIEntity entity = new(); + if (referencedCitations != null) + { + entity.Citation = referencedCitations; + } + + await turnContext.SendActivityAsync(new Activity() { - await turnContext.SendActivityAsync(command.Response.Content, null, null, cancellationToken); - }; + Type = ActivityTypes.Message, + Text = contentText, + ChannelData = channelData, + Entities = new List() { entity } + }, cancellationToken); return string.Empty; } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Augmentations/DefaultAugmentation.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Augmentations/DefaultAugmentation.cs index 6a6cbd973..b5e04badc 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Augmentations/DefaultAugmentation.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Augmentations/DefaultAugmentation.cs @@ -1,4 +1,5 @@ using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.AI.Models; using Microsoft.Teams.AI.AI.Planners; using Microsoft.Teams.AI.AI.Prompts; using Microsoft.Teams.AI.AI.Prompts.Sections; @@ -30,12 +31,22 @@ public DefaultAugmentation() /// public async Task CreatePlanFromResponseAsync(ITurnContext context, IMemory memory, PromptResponse response, CancellationToken cancellationToken = default) { - return await Task.FromResult(new Plan() + PredictedSayCommand say = new(response.Message?.Content ?? ""); + + if (response.Message != null) { - Commands = + ChatMessage message = new(ChatRole.Assistant) { - new PredictedSayCommand(response.Message?.Content ?? "") - } + Context = response.Message!.Context, + Content = response.Message.Content + }; + + say.Response = message; + } + + return await Task.FromResult(new Plan() + { + Commands = { say } }); } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs index 501be3a49..506f28778 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs @@ -8,6 +8,7 @@ using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; using Microsoft.Teams.AI.Utilities; +using Newtonsoft.Json.Linq; using System.Collections.Concurrent; using System.Text.RegularExpressions; @@ -776,6 +777,47 @@ public Application OnHandoff(HandoffHandler handler) return this; } + /// + /// Registers a handler for feedback loop events when a user clicks the thumbsup or thumbsdown button on a response sent from the AI module. + /// must be set to true. + /// + /// Function to cal lwhen the route is triggered + /// + public Application OnFeedbackLoop(FeedbackLoopHandler handler) + { + Verify.ParamNotNull(handler); + + RouteSelectorAsync routeSelector = (context, _) => + { + + string? actionName = (context.Activity.Value as JObject)?.GetValue("actionName")?.Value(); + return Task.FromResult + ( + context.Activity.Type == ActivityTypes.Invoke + && context.Activity.Name == "message/submitAction" + && actionName == "feedback" + ); + }; + + RouteHandler routeHandler = async (turnContext, turnState, cancellationToken) => + { + FeedbackLoopData feedbackLoopData = ActivityUtilities.GetTypedValue(turnContext.Activity)!; + feedbackLoopData.ReplyToId = turnContext.Activity.ReplyToId; + + await handler(turnContext, turnState, feedbackLoopData, cancellationToken); + + // Check to see if an invoke response has already been added + if (turnContext.TurnState.Get(BotAdapter.InvokeResponseKey) == null) + { + Activity activity = ActivityUtilities.CreateInvokeResponseActivity(); + await turnContext.SendActivityAsync(activity, cancellationToken); + } + }; + + AddRoute(routeSelector, routeHandler, isInvokeRoute: true); + return this; + } + /// /// Add a handler that will execute before the turn's activity handler logic is processed. ///
diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FeedbackLoopData.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FeedbackLoopData.cs new file mode 100644 index 000000000..36f58543b --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FeedbackLoopData.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; + +namespace Microsoft.Teams.AI.Application +{ + /// + /// Data returned when the thumbsup or thumbsdown button is clicked and response is received. + /// + public class FeedbackLoopData + { + /// + /// The action name. + /// + public string ActionName { get; set; } = "feedback"; + + /// + /// The action value. + /// + [JsonProperty(PropertyName = "actionValue")] + public FeedbackLoopDataActionValue? ActionValue { get; set; } + + /// + /// The activity ID that the feedback provided on. + /// + [JsonProperty(PropertyName = "replyToId")] + public string? ReplyToId { get; set; } + } + + /// + /// The feedback loop data's action value. + /// + public class FeedbackLoopDataActionValue + { + /// + /// Either "like" or "dislike" + /// + [JsonProperty(PropertyName = "reaction")] + public string? Reaction { get; set; } + + /// + /// The feedback provided by the user when prompted with "What did you lke/dislike?" + /// + [JsonProperty(PropertyName = "feedback")] + public string? Feedback { get; set; } + } +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FeedbackLoopHandler.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FeedbackLoopHandler.cs new file mode 100644 index 000000000..ac1639de2 --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FeedbackLoopHandler.cs @@ -0,0 +1,17 @@ +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.State; + +namespace Microsoft.Teams.AI.Application +{ + /// + /// Function for feedback loop activites + /// + /// Type of the turn state. This allows for strongly typed access to the turn state. + /// A strongly-typed context object for this turn. + /// The turn state object that stores arbitrary data for this turn. + /// The feedback loop data. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + public delegate Task FeedbackLoopHandler(ITurnContext turnContext, TState turnState, FeedbackLoopData feedbackLoopData, CancellationToken cancellationToken) where TState : TurnState; +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Utilities/CitationUtils.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Utilities/CitationUtils.cs new file mode 100644 index 000000000..f5fcb7a8d --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Utilities/CitationUtils.cs @@ -0,0 +1,71 @@ +using Microsoft.Teams.AI.AI.Action; +using System.Text.RegularExpressions; + +namespace Microsoft.Teams.AI.Utilities +{ + internal class CitationUtils + { + /// + /// Clips the text to a maximum length in case it exceeds the limit. + /// Replaces the last 3 characters with "..." + /// + /// The text to clip. + /// The max text length. Must be at least 4 characters long + /// The clipped text. + public static string Snippet(string text, int maxLength) + { + if (text.Length <= maxLength) + { + return text; + } + + string snippet = text.Substring(0, maxLength - 3).Trim(); + snippet += "..."; + return snippet; + } + + /// + /// Convert citation tags `[doc(s)n]` to `[n]` where n is a number. + /// + /// The text to format + /// The formatted text. + public static string FormatCitationsResponse(string text) + { + return Regex.Replace(text, @"\[docs?(\d+)\]", "[$1]", RegexOptions.IgnoreCase); + } + + /// + /// Filters out citations that are not referenced in the `text` as `[n]` tags (ex. `[1]` or `[2]`) + /// + /// Text that has citation tags. + /// List of citations + /// + public static List? GetUsedCitations(string text, List citations) + { + Regex regex = new(@"\[(\d+)\]"); + MatchCollection matches = regex.Matches(text); + + if (matches.Count == 0) + { + return null; + } + else + { + List usedCitations = []; + foreach (Match match in matches) + { + citations.Find((citation) => + { + if ($"[{citation.Position}]" == match.Value) + { + usedCitations.Add(citation); + return true; + } + return false; + }); + } + return usedCitations; + } + } + } +} diff --git a/dotnet/samples/08.datasource.azureopenai/.gitignore b/dotnet/samples/08.datasource.azureopenai/.gitignore index df9b07839..ae8ca341c 100644 --- a/dotnet/samples/08.datasource.azureopenai/.gitignore +++ b/dotnet/samples/08.datasource.azureopenai/.gitignore @@ -9,4 +9,4 @@ appsettings.TestTool.json **/env/ bin/Debug/ obj/ -appPackage/ \ No newline at end of file +appPackage/build/** \ No newline at end of file diff --git a/dotnet/samples/08.datasource.azureopenai/Program.cs b/dotnet/samples/08.datasource.azureopenai/Program.cs index 012510515..9601fc462 100644 --- a/dotnet/samples/08.datasource.azureopenai/Program.cs +++ b/dotnet/samples/08.datasource.azureopenai/Program.cs @@ -9,6 +9,7 @@ using AzureOpenAIBot; using Microsoft.Bot.Schema; using AdaptiveCards; +using Microsoft.Teams.AI.AI; var builder = WebApplication.CreateBuilder(args); @@ -93,7 +94,11 @@ loggerFactory: loggerFactory ); + AIOptions options = new(planner); + options.EnableFeedbackLoop = true; + Application app = new ApplicationBuilder() + .WithAIOptions(options) .WithStorage(sp.GetService()!) .Build(); @@ -103,17 +108,18 @@ await turnContext.SendActivityAsync("The conversation state has been reset"); }); - app.OnActivity(ActivityTypes.Message, async (turnContext, turnState, _) => + app.OnFeedbackLoop((turnContext, turnState, feedbackLoopData, _) => { - PromptTemplate template = prompts.GetPrompt("Chat"); - PromptResponse response = await planner.CompletePromptAsync(turnContext, turnState, template, null); - - Attachment card = new Attachment() + if (feedbackLoopData.ActionValue?.Reaction == "like") { - ContentType = AdaptiveCard.ContentType, - Content = ResponseCardCreator.CreateResponseCard(response) - }; - await turnContext.SendActivityAsync(MessageFactory.Attachment(card)); + Console.WriteLine("Like"); + } + else + { + Console.WriteLine("Dislike"); + } + + return Task.CompletedTask; }); return app; diff --git a/dotnet/samples/08.datasource.azureopenai/appPackage/color.png b/dotnet/samples/08.datasource.azureopenai/appPackage/color.png new file mode 100644 index 000000000..8a9a4a005 --- /dev/null +++ b/dotnet/samples/08.datasource.azureopenai/appPackage/color.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57b4440be2e604a74c52f0f38caac7191b9741ef0c48c9fcb2db2dc7feb8b000 +size 5098 diff --git a/dotnet/samples/08.datasource.azureopenai/appPackage/manifest.json b/dotnet/samples/08.datasource.azureopenai/appPackage/manifest.json new file mode 100644 index 000000000..e31ff9a7b --- /dev/null +++ b/dotnet/samples/08.datasource.azureopenai/appPackage/manifest.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.15/MicrosoftTeams.schema.json", + "version": "1.1.0", + "manifestVersion": "1.15", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.package.name", + "name": { + "short": "TeamsAzureOpenAI-${{TEAMSFX_ENV}}", + "full": "Teams Azure OpenAI" + }, + "developer": { + "name": "TeamsAzureOpenAI", + "mpnId": "", + "websiteUrl": "https://microsoft.com", + "privacyUrl": "https://privacy.microsoft.com/privacystatement", + "termsOfUseUrl": "https://www.microsoft.com/legal/terms-of-use" + }, + "description": { + "short": "Sample bot that can do RAG through Azure OpenAI On Your Data", + "full": "Sample bot that can do RAG through Azure OpenAI On Your Data" + }, + "icons": { + "outline": "outline.png", + "color": "color.png" + }, + "accentColor": "#FFFFFF", + "staticTabs": [ + { + "entityId": "conversations", + "scopes": ["personal"] + }, + { + "entityId": "about", + "scopes": ["personal"] + } + ], + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": ["personal", "team", "groupChat"], + "isNotificationOnly": false, + "supportsCalling": false, + "supportsVideo": false, + "supportsFiles": false + } + ], + "validDomains": [] +} diff --git a/dotnet/samples/08.datasource.azureopenai/appPackage/outline.png b/dotnet/samples/08.datasource.azureopenai/appPackage/outline.png new file mode 100644 index 000000000..b4123e1f2 --- /dev/null +++ b/dotnet/samples/08.datasource.azureopenai/appPackage/outline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de215c72bbca73d2ebd301f81faaa2768786d62baf48ee45eb8edf6252d323e6 +size 852 diff --git a/getting-started/CONCEPTS/POWERED-BY-AI.md b/getting-started/CONCEPTS/POWERED-BY-AI.md index b0419631e..2b01305d2 100644 --- a/getting-started/CONCEPTS/POWERED-BY-AI.md +++ b/getting-started/CONCEPTS/POWERED-BY-AI.md @@ -72,6 +72,7 @@ Feedback loop, when enabled, will add thumbs up and thumbs down buttons to the b To enable this feature, set the `enable_feedback_loop` property to `true` in the `AIOptions` object when creating the `AI` object. +### JS ```typescript export const app = new Application({ ai: { @@ -80,6 +81,17 @@ export const app = new Application({ }, ``` +### C# +```csharp +AIOptions options = new(planner); +options.EnableFeedbackLoop = true; // setting `EnableFeedbackLoop` + +Application app = new ApplicationBuilder() + .WithAIOptions(options) + .Build(); +``` + + This feature is set to false by default. When enabled, all SAY commands from the AI will have the following added to the activity: ```jsonc @@ -97,12 +109,21 @@ If the user presses either of the feedback buttons, they will be prompted to pro Use the `app.feedbackLoop` method to register a feedback loop handler. This method will be called when the user provides feedback on the AI system's response. It is up to the developer to store and process the feedback. +### JS ```typescript app.feedbackLoop(async (context, state, feedbackLoopData) => { // custom logic here... }); ``` +### C# +```csharp +app.OnFeedbackLoop(async (turnContext, turnState, feedbackLoopData, cancellationToken) => +{ + // custom logic here... +}); +``` + The `feedbackLoopData` payload will contain the following properties: ```jsonc @@ -147,3 +168,20 @@ app.ai.action(AI.SayCommandActionName, async (context, stat return ""; }); ``` + +### C# +```csharp +// AIActions.cs +public class AIActions +{ + [Action(AIConstants.SayCommandActionName, isDefault: false)] + public async Task SayCommandAsync([ActionTurnContext] ITurnContext turnContext, [ActionParameters] PredictedSayCommand command, CancellationToken cancellationToken = default) + { + // Custom logic here... + return ""; + } +} + +// Program.cs +app.AI.ImportActions(new AIActions()); +```