From df5477921bff4439b921f4cde032aa7c09de2a4b Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Tue, 7 Oct 2025 11:54:41 -0700 Subject: [PATCH] .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 --- dotnet/agent-framework-dotnet.slnx | 5 + ...entOrchestrations_Step01_Concurrent.csproj | 26 ++ .../Step01_Concurrent/Program.cs | 109 ++++++++ ...entOrchestrations_Step02_Sequential.csproj | 26 ++ .../Step02_Sequential/Program.cs | 112 ++++++++ .../AgentOrchestrations_Step03_Handoff.csproj | 26 ++ .../Step03_Handoff/Program.cs | 247 ++++++++++++++++++ 7 files changed, 551 insertions(+) create mode 100644 dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/AgentOrchestrations_Step01_Concurrent.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/AgentOrchestrations_Step02_Sequential.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/AgentOrchestrations_Step03_Handoff.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/Program.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 19fcb08fe7..5bfd03775e 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -171,6 +171,11 @@ + + + + + diff --git a/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/AgentOrchestrations_Step01_Concurrent.csproj b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/AgentOrchestrations_Step01_Concurrent.csproj new file mode 100644 index 0000000000..a548ac03d0 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/AgentOrchestrations_Step01_Concurrent.csproj @@ -0,0 +1,26 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707;VSTHRD200 + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/Program.cs b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/Program.cs new file mode 100644 index 0000000000..ac6c276434 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step01_Concurrent/Program.cs @@ -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 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 diff --git a/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/AgentOrchestrations_Step02_Sequential.csproj b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/AgentOrchestrations_Step02_Sequential.csproj new file mode 100644 index 0000000000..a548ac03d0 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/AgentOrchestrations_Step02_Sequential.csproj @@ -0,0 +1,26 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707;VSTHRD200 + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/Program.cs b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/Program.cs new file mode 100644 index 0000000000..e317ac0ff8 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step02_Sequential/Program.cs @@ -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 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 diff --git a/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/AgentOrchestrations_Step03_Handoff.csproj b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/AgentOrchestrations_Step03_Handoff.csproj new file mode 100644 index 0000000000..a548ac03d0 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/AgentOrchestrations_Step03_Handoff.csproj @@ -0,0 +1,26 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707;VSTHRD200 + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/Program.cs b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/Program.cs new file mode 100644 index 0000000000..f66fefe535 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AgentOrchestrations/Step03_Handoff/Program.cs @@ -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 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 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 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().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 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().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>()!); + } + } + } +} +# endregion