// Copyright (c) Microsoft. All rights reserved. using System.Text.Json; using AdaptiveCards; using M365Agent.Agents; using Microsoft.Agents.AI; using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.App; using Microsoft.Agents.Builder.State; using Microsoft.Agents.Core.Models; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; namespace M365Agent; /// /// An adapter class that exposes a Microsoft Agent Framework as a M365 Agent SDK . /// internal sealed class AFAgentApplication : AgentApplication { private readonly AIAgent _agent; private readonly string? _welcomeMessage; public AFAgentApplication(AIAgent agent, AgentApplicationOptions options, [FromKeyedServices("AFAgentApplicationWelcomeMessage")] string? welcomeMessage = null) : base(options) { this._agent = agent; this._welcomeMessage = welcomeMessage; this.OnConversationUpdate(ConversationUpdateEvents.MembersAdded, this.WelcomeMessageAsync); this.OnActivity(ActivityTypes.Message, this.MessageActivityAsync, rank: RouteRank.Last); } /// /// The main agent invocation method, where each user message triggers a call to the underlying . /// private async Task MessageActivityAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { // Start a Streaming Process await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Working on a response for you", cancellationToken); // Get the conversation history from turn state. JsonElement threadElementStart = turnState.GetValue("conversation.chatHistory"); // Deserialize the conversation history into an AgentThread, or create a new one if none exists. AgentThread agentThread = threadElementStart.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null ? await this._agent.DeserializeThreadAsync(threadElementStart, JsonUtilities.DefaultOptions, cancellationToken) : await this._agent.GetNewThreadAsync(cancellationToken); ChatMessage chatMessage = HandleUserInput(turnContext); // Invoke the WeatherForecastAgent to process the message AgentResponse agentResponse = await this._agent.RunAsync(chatMessage, agentThread, cancellationToken: cancellationToken); // Check for any user input requests in the response // and turn them into adaptive cards in the streaming response. List? attachments = null; HandleUserInputRequests(agentResponse, ref attachments); // Check for Adaptive Card content in the response messages // and return them appropriately in the response. var adaptiveCards = agentResponse.Messages.SelectMany(x => x.Contents).OfType().ToList(); if (adaptiveCards.Count > 0) { attachments ??= []; attachments.Add(new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = adaptiveCards.First().AdaptiveCardJson, }); } else { turnContext.StreamingResponse.QueueTextChunk(agentResponse.Text); } // If created any adaptive cards, add them to the final message. if (attachments is not null) { turnContext.StreamingResponse.FinalMessage = MessageFactory.Attachment(attachments); } // Serialize and save the updated conversation history back to turn state. JsonElement threadElementEnd = agentThread.Serialize(JsonUtilities.DefaultOptions); turnState.SetValue("conversation.chatHistory", threadElementEnd); // End the streaming response await turnContext.StreamingResponse.EndStreamAsync(cancellationToken); } /// /// A method to show a welcome message when a new user joins the conversation. /// private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(this._welcomeMessage)) { return; } foreach (ChannelAccount member in turnContext.Activity.MembersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { await turnContext.SendActivityAsync(MessageFactory.Text(this._welcomeMessage), cancellationToken); } } } /// /// When a user responds to a function approval request by clicking on a card, this method converts the response /// into the appropriate approval or rejection . /// /// The for the current turn. /// The to pass to the . private static ChatMessage HandleUserInput(ITurnContext turnContext) { // Check if this contains the function approval Adaptive Card response. if (turnContext.Activity.Value is JsonElement valueElement && valueElement.GetProperty("type").GetString() == "functionApproval" && valueElement.GetProperty("approved") is JsonElement approvedJsonElement && approvedJsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False && valueElement.GetProperty("requestJson") is JsonElement requestJsonElement && requestJsonElement.ValueKind == JsonValueKind.String) { var requestContent = JsonSerializer.Deserialize(requestJsonElement.GetString()!, JsonUtilities.DefaultOptions); return new ChatMessage(ChatRole.User, [requestContent!.CreateResponse(approvedJsonElement.ValueKind == JsonValueKind.True)]); } return new ChatMessage(ChatRole.User, turnContext.Activity.Text); } /// /// When the agent returns any user input requests, this method converts them into adaptive cards that /// asks the user to approve or deny the requests. /// /// The that may contain the user input requests. /// The list of to which the adaptive cards will be added. private static void HandleUserInputRequests(AgentResponse response, ref List? attachments) { var userInputRequests = response.UserInputRequests.ToList(); if (userInputRequests.Count > 0) { foreach (var functionApprovalRequest in userInputRequests.OfType()) { var functionApprovalRequestJson = JsonSerializer.Serialize(functionApprovalRequest, JsonUtilities.DefaultOptions); var card = new AdaptiveCard("1.5"); card.Body.Add(new AdaptiveTextBlock { Text = "Function Call Approval Required", Size = AdaptiveTextSize.Large, Weight = AdaptiveTextWeight.Bolder, HorizontalAlignment = AdaptiveHorizontalAlignment.Center }); card.Body.Add(new AdaptiveTextBlock { Text = $"Function: {functionApprovalRequest.FunctionCall.Name}" }); card.Body.Add(new AdaptiveActionSet() { Actions = [ new AdaptiveSubmitAction { Id = "Approve", Title = "Approve", Data = new { type = "functionApproval", approved = true, requestJson = functionApprovalRequestJson } }, new AdaptiveSubmitAction { Id = "Deny", Title = "Deny", Data = new { type = "functionApproval", approved = false, requestJson = functionApprovalRequestJson } } ] }); attachments ??= []; attachments.Add(new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = card.ToJson(), }); } } } }