Files
agent-framework/dotnet/samples/M365Agent/AFAgentApplication.cs
westey 6a88c7b1a1 .NET: [BREAKING] Change SerializeSession to be Async (#3879)
* Change SerializeSession to be Async

* Update Changelog
2026-02-12 11:56:42 +00:00

185 lines
8.3 KiB
C#

// 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;
/// <summary>
/// An adapter class that exposes a Microsoft Agent Framework <see cref="AIAgent"/> as a M365 Agent SDK <see cref="AgentApplication"/>.
/// </summary>
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);
}
/// <summary>
/// The main agent invocation method, where each user message triggers a call to the underlying <see cref="AIAgent"/>.
/// </summary>
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 sessionElementStart = turnState.GetValue<JsonElement>("conversation.chatHistory");
// Deserialize the conversation history into an AgentSession, or create a new one if none exists.
AgentSession agentSession = sessionElementStart.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null
? await this._agent.DeserializeSessionAsync(sessionElementStart, JsonUtilities.DefaultOptions, cancellationToken)
: await this._agent.CreateSessionAsync(cancellationToken);
ChatMessage chatMessage = HandleUserInput(turnContext);
// Invoke the WeatherForecastAgent to process the message
AgentResponse agentResponse = await this._agent.RunAsync(chatMessage, agentSession, cancellationToken: cancellationToken);
// Check for any user input requests in the response
// and turn them into adaptive cards in the streaming response.
List<Attachment>? 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<AdaptiveCardAIContent>().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 sessionElementEnd = await this._agent.SerializeSessionAsync(agentSession, JsonUtilities.DefaultOptions, cancellationToken);
turnState.SetValue("conversation.chatHistory", sessionElementEnd);
// End the streaming response
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken);
}
/// <summary>
/// A method to show a welcome message when a new user joins the conversation.
/// </summary>
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);
}
}
}
/// <summary>
/// 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 <see cref="ChatMessage"/>.
/// </summary>
/// <param name="turnContext">The <see cref="ITurnContext"/> for the current turn.</param>
/// <returns>The <see cref="ChatMessage"/> to pass to the <see cref="AIAgent"/>.</returns>
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<FunctionApprovalRequestContent>(requestJsonElement.GetString()!, JsonUtilities.DefaultOptions);
return new ChatMessage(ChatRole.User, [requestContent!.CreateResponse(approvedJsonElement.ValueKind == JsonValueKind.True)]);
}
return new ChatMessage(ChatRole.User, turnContext.Activity.Text);
}
/// <summary>
/// When the agent returns any function approval requests, this method converts them into adaptive cards that
/// asks the user to approve or deny the requests.
/// </summary>
/// <param name="response">The <see cref="AgentResponse"/> that may contain the function approval requests.</param>
/// <param name="attachments">The list of <see cref="Attachment"/> to which the adaptive cards will be added.</param>
private static void HandleUserInputRequests(AgentResponse response, ref List<Attachment>? attachments)
{
foreach (FunctionApprovalRequestContent functionApprovalRequest in response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>())
{
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(),
});
}
}
}