.NET: Add Orchestration SK->AF migration samples (#1044)

* Add Orchestration SK->AF migration samples

* fix samples

* clean up

* Add handoff

* Comments

* Address comments

* Fix build error

* Comments
This commit is contained in:
Tao Chen
2025-10-07 11:54:41 -07:00
committed by GitHub
Unverified
parent 93577f76b1
commit df5477921b
7 changed files with 551 additions and 0 deletions
+5
View File
@@ -171,6 +171,11 @@
<Project Path="samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/AzureOpenAIResponses_Step03_ToolCall.csproj" />
<Project Path="samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/AzureOpenAIResponses_Step04_DependencyInjection.csproj" />
</Folder>
<Folder Name="/Samples/SemanticKernelMigration/AgentOrchestrations/">
<Project Path="samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/AgentOrchestrations_Step01_Concurrent.csproj" />
<Project Path="samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/AgentOrchestrations_Step02_Sequential.csproj" />
<Project Path="samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/AgentOrchestrations_Step03_Handoff.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CA1812;RCS1102;CA1707;VSTHRD200</NoWarn>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" VersionOverride="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.AzureAI" VersionOverride="1.*-*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" VersionOverride="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Orchestration" VersionOverride="1.*-*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" VersionOverride="1.*-*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Orchestration;
using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var agentInstructions = "You are a translation assistant who only responds in {0}. Respond to any input by outputting the name of the input language and then translating the input to {0}.";
// This sample compares running concurrent orchestrations using
// Semantic Kernel and the Agent Framework.
Console.WriteLine("=== Semantic Kernel Concurrent Orchestration ===");
await SKConcurrentOrchestration();
Console.WriteLine("\n=== Agent Framework Concurrent Agent Workflow ===");
await AFConcurrentAgentWorkflow();
# region SKConcurrentOrchestration
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
async Task SKConcurrentOrchestration()
{
ConcurrentOrchestration orchestration = new([
GetSKTranslationAgent("French"),
GetSKTranslationAgent("Spanish")])
{
StreamingResponseCallback = StreamingResultCallback,
};
InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
OrchestrationResult<string[]> result = await orchestration.InvokeAsync("Hello, world!", runtime);
string[] texts = await result.GetValueAsync(TimeSpan.FromSeconds(20));
await runtime.RunUntilIdleAsync();
}
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
ChatCompletionAgent GetSKTranslationAgent(string targetLanguage)
{
var kernel = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(deploymentName, endpoint, new AzureCliCredential()).Build();
return new ChatCompletionAgent()
{
Kernel = kernel,
Instructions = string.Format(agentInstructions, targetLanguage),
Description = $"Agent that translates texts to {targetLanguage}",
Name = $"SKTranslationAgent_{targetLanguage}"
};
}
ValueTask StreamingResultCallback(StreamingChatMessageContent streamedResponse, bool isFinal)
{
Console.Write(streamedResponse.Content);
if (isFinal)
{
Console.WriteLine();
}
return ValueTask.CompletedTask;
}
# endregion
# region AFConcurrentAgentWorkflow
async Task AFConcurrentAgentWorkflow()
{
var client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
var frenchAgent = GetAFTranslationAgent("French", client);
var spanishAgent = GetAFTranslationAgent("Spanish", client);
var concurrentAgentWorkflow = AgentWorkflowBuilder.BuildConcurrent([frenchAgent, spanishAgent]);
await using StreamingRun run = await InProcessExecution.StreamAsync(concurrentAgentWorkflow, "Hello, world!");
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is AgentRunUpdateEvent e)
{
if (string.IsNullOrEmpty(e.Update.Text))
{
continue;
}
if (e.ExecutorId != lastExecutorId)
{
lastExecutorId = e.ExecutorId;
Console.WriteLine();
Console.Write($"{e.Update.AuthorName}: ");
}
Console.Write(e.Update.Text);
}
}
}
ChatClientAgent GetAFTranslationAgent(string targetLanguage, IChatClient chatClient) =>
new(chatClient, string.Format(agentInstructions, targetLanguage), name: $"AFTranslationAgent_{targetLanguage}");
# endregion
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CA1812;RCS1102;CA1707;VSTHRD200</NoWarn>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" VersionOverride="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.AzureAI" VersionOverride="1.*-*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" VersionOverride="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Orchestration" VersionOverride="1.*-*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" VersionOverride="1.*-*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,112 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Orchestration;
using Microsoft.SemanticKernel.Agents.Orchestration.Sequential;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var agentInstructions = "You are a translation assistant who only responds in {0}. Respond to any input by outputting the name of the input language and then translating the input to {0}.";
// This sample compares running sequential orchestrations using
// Semantic Kernel and the Agent Framework.
Console.WriteLine("=== Semantic Kernel Sequential Orchestration ===");
await SKSequentialOrchestration();
Console.WriteLine("\n=== Agent Framework Sequential Agent Workflow ===");
await AFSequentialAgentWorkflow();
# region SKSequentialOrchestration
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
async Task SKSequentialOrchestration()
{
SequentialOrchestration orchestration = new([
GetSKTranslationAgent("French"),
GetSKTranslationAgent("Spanish"),
GetSKTranslationAgent("English")])
{
StreamingResponseCallback = StreamingResultCallback,
};
InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
OrchestrationResult<string> result = await orchestration.InvokeAsync("Hello, world!", runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(20));
await runtime.RunUntilIdleAsync();
}
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
ChatCompletionAgent GetSKTranslationAgent(string targetLanguage)
{
var kernel = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(deploymentName, endpoint, new AzureCliCredential()).Build();
return new ChatCompletionAgent()
{
Kernel = kernel,
Instructions = string.Format(agentInstructions, targetLanguage),
Description = $"Agent that translates texts to {targetLanguage}",
Name = $"SKTranslationAgent_{targetLanguage}"
};
}
ValueTask StreamingResultCallback(StreamingChatMessageContent streamedResponse, bool isFinal)
{
Console.Write(streamedResponse.Content);
if (isFinal)
{
Console.WriteLine();
}
return ValueTask.CompletedTask;
}
# endregion
# region AFSequentialAgentWorkflow
async Task AFSequentialAgentWorkflow()
{
var client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
var frenchAgent = GetAFTranslationAgent("French", client);
var spanishAgent = GetAFTranslationAgent("Spanish", client);
var englishAgent = GetAFTranslationAgent("English", client);
var sequentialAgentWorkflow = AgentWorkflowBuilder.BuildSequential(
[frenchAgent, spanishAgent, englishAgent]);
await using StreamingRun run = await InProcessExecution.StreamAsync(sequentialAgentWorkflow, "Hello, world!");
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is AgentRunUpdateEvent e)
{
if (string.IsNullOrEmpty(e.Update.Text))
{
continue;
}
if (e.ExecutorId != lastExecutorId)
{
lastExecutorId = e.ExecutorId;
Console.WriteLine();
Console.Write($"{e.Update.AuthorName}: ");
}
Console.Write(e.Update.Text);
}
}
}
ChatClientAgent GetAFTranslationAgent(string targetLanguage, IChatClient chatClient) =>
new(chatClient, string.Format(agentInstructions, targetLanguage), name: $"AFTranslationAgent_{targetLanguage}");
# endregion
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CA1812;RCS1102;CA1707;VSTHRD200</NoWarn>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" VersionOverride="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.AzureAI" VersionOverride="1.*-*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" VersionOverride="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Orchestration" VersionOverride="1.*-*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" VersionOverride="1.*-*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,247 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Orchestration;
using Microsoft.SemanticKernel.Agents.Orchestration.Handoff;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
using Microsoft.SemanticKernel.ChatCompletion;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Queries to simulate user input during the interactive orchestration
List<string> Queries = [
"I'd like to track the status of my first order 123.",
"I want to return another order of mine whose ID is 456 because it arrived damaged.",
];
// This sample compares running handoff orchestrations using
// Semantic Kernel and the Agent Framework.
Console.WriteLine("=== Semantic Kernel Handoff Orchestration ===");
// State to help format the streaming output
bool newAgentTurn = true;
string previousFunctionCallId = string.Empty;
await SKHandoffOrchestration();
Console.WriteLine("\n=== Agent Framework Handoff Agent Workflow ===");
await AFHandoffAgentWorkflow();
# region SKHandoffOrchestration
[KernelFunction]
string SKCheckOrderStatus(string orderId) => $"Order {orderId} is shipped and will arrive in 2-3 days.";
[KernelFunction]
string SKProcessReturn(string orderId, string reason) => $"Return for order {orderId} has been processed successfully.";
[KernelFunction]
string SKProcessRefund(string orderId, string reason) => $"Refund for order {orderId} has been processed successfully.";
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
async Task SKHandoffOrchestration()
{
// Create agents
var triageAgent = GetSKAgent(
instructions: "You are a customer support agent that triages issues.",
name: "TriageAgent",
description: "Handle customer requests.");
var statusAgent = GetSKAgent(
instructions: "You are a customer support agent that checks order status.",
name: "OrderStatusAgent",
description: "Handle order status requests.");
statusAgent.Kernel.Plugins.AddFromFunctions("OrderStatusPlugin", [KernelFunctionFactory.CreateFromMethod(SKCheckOrderStatus)]);
var returnAgent = GetSKAgent(
instructions: "You are a customer support agent that handles order returns.",
name: "OrderReturnAgent",
description: "Handle order return requests.");
returnAgent.Kernel.Plugins.AddFromFunctions("OrderReturnPlugin", [KernelFunctionFactory.CreateFromMethod(SKProcessReturn)]);
var refundAgent = GetSKAgent(
instructions: "You are a customer support agent that handles order refunds.",
name: "OrderRefundAgent",
description: "Handle order refund requests.");
refundAgent.Kernel.Plugins.AddFromFunctions("OrderRefundPlugin", [KernelFunctionFactory.CreateFromMethod(SKProcessRefund)]);
Queue<string> queries = new(Queries);
// Create orchestration with handoffs
HandoffOrchestration orchestration =
new(OrchestrationHandoffs
.StartWith(triageAgent)
.Add(triageAgent, statusAgent, returnAgent, refundAgent)
.Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related")
.Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related")
.Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related"),
triageAgent,
statusAgent,
returnAgent,
refundAgent)
{
InteractiveCallback = () =>
{
string input = queries.Count > 0 ? queries.Dequeue() : "exit";
Console.WriteLine($"\nUser: {input}");
return ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input));
},
StreamingResponseCallback = StreamingResultCallback,
};
InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
OrchestrationResult<string> result = await orchestration.InvokeAsync(
"I am a customer that needs help with my two orders",
runtime);
string text = await result.GetValueAsync();
Console.WriteLine($"\nFinal Result: {text}");
await runtime.RunUntilIdleAsync();
}
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
ChatCompletionAgent GetSKAgent(string instructions, string name, string description)
{
var kernel = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(deploymentName, endpoint, new AzureCliCredential()).Build();
return new ChatCompletionAgent()
{
Kernel = kernel,
Instructions = instructions,
Description = description,
Name = name
};
}
ValueTask StreamingResultCallback(StreamingChatMessageContent streamedResponse, bool isFinal)
{
if (newAgentTurn)
{
Console.Write($"\n{streamedResponse.AuthorName}: ");
newAgentTurn = false;
}
Console.Write(streamedResponse.Content);
if (streamedResponse.Items.OfType<StreamingFunctionCallUpdateContent>().FirstOrDefault()
is StreamingFunctionCallUpdateContent call)
{
if (call.CallId is not null && previousFunctionCallId != call.CallId)
{
Console.Write($"\nCalling function '{call.Name}' with arguments: ");
previousFunctionCallId = call.CallId;
}
if (!string.IsNullOrEmpty(call.Arguments))
{
Console.Write($"{call.Arguments}");
}
}
if (isFinal)
{
newAgentTurn = true;
previousFunctionCallId = string.Empty;
Console.WriteLine();
}
return ValueTask.CompletedTask;
}
# endregion
# region AFHandoffAgentWorkflow
[Description("Get the order status for a given order ID.")]
static string AFCheckOrderStatus([Description("The order ID to check the status for.")] string orderId)
=> $"Order {orderId} is shipped and will arrive in 2-3 days.";
[Description("Process a return for a given order ID.")]
static string AFProcessReturn(
[Description("The order ID to process the return for.")] string orderId,
[Description("The reason for the return.")] string reason)
=> $"Return for order {orderId} has been processed successfully for the following reason: {reason}.";
[Description("Process a refund for a given order ID.")]
static string AFProcessRefund([Description("The order ID to process the refund for.")] string orderId)
=> $"Refund for order {orderId} has been processed successfully.";
async Task AFHandoffAgentWorkflow()
{
// Create agents
var client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
ChatClientAgent triageAgent = new(client,
instructions: "A customer support agent that triages issues.",
name: "TriageAgent",
description: "Handle customer requests.");
ChatClientAgent statusAgent = new(client,
name: "OrderStatusAgent",
instructions: "Handle order status requests.",
description: "A customer support agent that checks order status.",
tools: [AIFunctionFactory.Create(AFCheckOrderStatus)]);
ChatClientAgent returnAgent = new(client,
name: "OrderReturnAgent",
instructions: "Handle order return requests.",
description: "A customer support agent that handles order returns.",
tools: [AIFunctionFactory.Create(AFProcessReturn)]);
ChatClientAgent refundAgent = new(client,
name: "OrderRefundAgent",
instructions: "Handle order refund requests.",
description: "A customer support agent that handles order refund.",
tools: [AIFunctionFactory.Create(AFProcessRefund)]);
// Create workflow with handoffs
var handoffAgentWorkflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
.WithHandoffs(triageAgent, [statusAgent, returnAgent, refundAgent])
.WithHandoff(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related")
.WithHandoff(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related")
.WithHandoff(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related")
.Build();
// Run the workflow
List<ChatMessage> messages = [];
foreach (var query in Queries)
{
Console.WriteLine($"User: {query}");
messages.Add(new(ChatRole.User, query));
await using var run = await InProcessExecution.StreamAsync(handoffAgentWorkflow, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is AgentRunUpdateEvent e)
{
if (string.IsNullOrEmpty(e.Update.Text) && e.Update.Contents.Count == 0)
{
continue;
}
if (e.ExecutorId != lastExecutorId)
{
lastExecutorId = e.ExecutorId;
Console.WriteLine();
Console.Write($"{e.Update.AuthorName}: ");
}
Console.Write(e.Update.Text);
if (e.Update.Contents.OfType<Microsoft.Extensions.AI.FunctionCallContent>().FirstOrDefault()
is Microsoft.Extensions.AI.FunctionCallContent call)
{
Console.WriteLine();
Console.WriteLine($"Calling function '{call.Name}' with arguments: {JsonSerializer.Serialize(call.Arguments)}");
}
}
else if (evt is WorkflowOutputEvent output)
{
Console.WriteLine("\n");
messages.AddRange(output.As<List<ChatMessage>>()!);
}
}
}
}
# endregion