mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
6a88c7b1a1
* Change SerializeSession to be Async * Update Changelog
185 lines
8.3 KiB
C#
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(),
|
|
});
|
|
}
|
|
}
|
|
}
|