From 499838565ef7d79c9766deb829c23e2dbeb6636a Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Sat, 27 Sep 2025 07:48:26 +0100 Subject: [PATCH] .NET: Fix samples, improve readme language and add readme to sln. (#942) * Fix samples, improve readme language and add readme to sln. * Fix threading in openai samples too. --- dotnet/agent-framework-dotnet.slnx | 4 +- .../Step01_Basics/Program.cs | 7 +- .../Step02_ReasoningModel/Program.cs | 7 +- .../OpenAIResponses/Step01_Basics/Program.cs | 5 +- .../Step02_ReasoningModel/Program.cs | 5 +- .../samples/SemanticKernelMigration/README.md | 106 +++++++++++------- 6 files changed, 80 insertions(+), 54 deletions(-) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 47c36754fe..eda84f1e77 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -112,7 +112,9 @@ - + + + diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs index a89b05905d..8c7c6ab13f 100644 --- a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs @@ -22,8 +22,9 @@ async Task SKAgentAsync() { Console.WriteLine("\n=== SK Agent ===\n"); - OpenAIResponseAgent agent = new(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) - .GetOpenAIResponseClient(deploymentName)) + var responseClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName); + OpenAIResponseAgent agent = new(responseClient) { Name = "Joker", Instructions = "You are good at telling jokes.", @@ -31,7 +32,7 @@ async Task SKAgentAsync() var agentOptions = new OpenAIResponseAgentInvokeOptions() { ResponseCreationOptions = new() { MaxOutputTokenCount = 1000 } }; - Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + Microsoft.SemanticKernel.Agents.AgentThread? thread = new OpenAIResponseAgentThread(responseClient); await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) { Console.WriteLine(item.Message); diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs index ddfc95b7e9..1d6cc633ba 100644 --- a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs @@ -49,8 +49,9 @@ async Task SKAgentAsync() { Console.WriteLine("\n=== SK Agent ===\n"); - OpenAIResponseAgent agent = new(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) - .GetOpenAIResponseClient(deploymentName)) + var responseClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName); + OpenAIResponseAgent agent = new(responseClient) { Name = "Joker", Instructions = "You are good at telling jokes.", @@ -69,7 +70,7 @@ async Task SKAgentAsync() } }; - Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + Microsoft.SemanticKernel.Agents.AgentThread? thread = new OpenAIResponseAgentThread(responseClient); await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) { foreach (var content in item.Message.Items) diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs index d4472eee53..d88684e240 100644 --- a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs @@ -20,7 +20,8 @@ async Task SKAgentAsync() { Console.WriteLine("\n=== SK Agent ===\n"); - OpenAIResponseAgent agent = new(new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId)) + var responseClient = new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId); + OpenAIResponseAgent agent = new(responseClient) { Name = "Joker", Instructions = "You are good at telling jokes.", @@ -28,7 +29,7 @@ async Task SKAgentAsync() var agentOptions = new OpenAIResponseAgentInvokeOptions() { ResponseCreationOptions = new() { MaxOutputTokenCount = 1000 } }; - Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + Microsoft.SemanticKernel.Agents.AgentThread? thread = new OpenAIResponseAgentThread(responseClient); await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) { Console.WriteLine(item.Message); diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs index cab7d2d288..818604fd64 100644 --- a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs @@ -47,7 +47,8 @@ async Task SKAgentAsync() { Console.WriteLine("\n=== SK Agent ===\n"); - OpenAIResponseAgent agent = new(new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId)) + var responseClient = new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId); + OpenAIResponseAgent agent = new(responseClient) { Name = "Joker", Instructions = "You are good at telling jokes.", @@ -66,7 +67,7 @@ async Task SKAgentAsync() } }; - Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + Microsoft.SemanticKernel.Agents.AgentThread? thread = new OpenAIResponseAgentThread(responseClient); await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) { foreach (var content in item.Message.Items) diff --git a/dotnet/samples/SemanticKernelMigration/README.md b/dotnet/samples/SemanticKernelMigration/README.md index be27d6260b..e2451fd083 100644 --- a/dotnet/samples/SemanticKernelMigration/README.md +++ b/dotnet/samples/SemanticKernelMigration/README.md @@ -1,7 +1,7 @@ # Semantic Kernel to Agent Framework Migration Guide ## What's Changed? -- **Namespace Updates**: From `Microsoft.SemanticKernel.Agents` to `Microsoft.Extensions.AI.Agents` +- **Namespace Updates**: From `Microsoft.SemanticKernel.Agents` to `Microsoft.Agents.AI` - **Agent Creation**: Single fluent API calls vs multi-step builder patterns - **Thread Management**: Built-in thread management vs manual thread creation - **Tool Registration**: Direct function registration vs plugin wrapper systems @@ -27,12 +27,8 @@ using Microsoft.SemanticKernel.Agents; #### Agent Framework -Agent Framework namespaces are now under `Microsoft.Extensions.AI`. - -- `Microsoft.Extensions.AI` for core AI types -- `Microsoft.Extensions.AI.Agents` for core agent types -OR just -- `Microsoft.Extensions.AI.Agents.Abstractions` if your +Agent Framework namespaces are under `Microsoft.Agents.AI`. +Agent Framework uses the core AI message and content types from `Microsoft.Extensions.AI` for communication between components. ```csharp using Microsoft.Extensions.AI; @@ -54,7 +50,7 @@ an empty `Kernel` if not provided. ChatCompletionAgent agent = new() { Instructions = ParrotInstructions, Kernel = kernel }; ``` -Azure AI Foundry requires a strong setup before creating an agent +Azure AI Foundry requires an agent resource to be created in the cloud before creating a local agent class that uses it. ```csharp PersistentAgentsClient azureAgentClient = AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential()); @@ -72,8 +68,8 @@ Agent creation in Agent Framework is made simpler with extensions provided by al ```csharp AIAgent openAIAgent = chatClient.CreateAIAgent(instructions: ParrotInstructions); -AIAgent azureFoundryAgent = persistentAgentsClient.CreateAIAgent(instructions: ParrotInstructions); -AIAgent openAIAssistantAgent = assistantClient.CreateAIAgent(instructions: ParrotInstructions); +AIAgent azureFoundryAgent = await persistentAgentsClient.CreateAIAgentAsync(instructions: ParrotInstructions); +AIAgent openAIAssistantAgent = await assistantClient.CreateAIAgentAsync(instructions: ParrotInstructions); ``` Additionally for hosted agent providers you can also use the `GetAIAgent` to retrieve an agent from an existing hosted agent. @@ -82,8 +78,7 @@ Additionally for hosted agent providers you can also use the `GetAIAgent` to ret AIAgent azureFoundryAgent = await persistentAgentsClient.GetAIAgentAsync(agentId); ``` - -### Agent Thread Creation +### 3. Agent Thread Creation #### Semantic Kernel @@ -94,7 +89,7 @@ The caller has to know the thread type and create it manually. AgentThread thread = new OpenAIAssistantAgentThread(this.AssistantClient); AgentThread thread = new AzureAIAgentThread(this.Client); AgentThread thread = new OpenAIResponseAgentThread(this.Client); -``` +``` #### Agent Framework @@ -105,7 +100,7 @@ The agent is responsible for creating the thread. AgentThread thread = agent.GetNewThread(); ``` -### Hosted Agent Thread Cleanup +### 4. Hosted Agent Thread Cleanup This case applies exclusively to a few AI providers that still provide hosted threads. @@ -121,28 +116,26 @@ await thread.DeleteAsync(); #### Agent Framework > [!NOTE] -> OpenAI Responses introduced a new conversation model that simplifies completely how conversations are handled avoiding any previous hosted thread management complexities that were initially introduced by the now deprecated OpenAI Assistants model well documented in https://platform.openai.com/docs/assistants/migration +> OpenAI Responses introduced a new conversation model that simplifies how conversations are handled. This simplifies hosted thread management compared to the now deprecated OpenAI Assistants model. For more information see the [OpenAI Assistants migration guide](https://platform.openai.com/docs/assistants/migration). +Agent Framework doesn't have a thread deletion API in the `AgentThread` type as not all providers support hosted threads or thread deletion and this will become more common as more providers shift to responses based architectures. - -Agent Framework doesn't have thread deletion API in the `AgentThread` type as not all providers require hosted thread cleanup and this will become more common as more providers shift to conversation based architectures. - -**When the provider allow thread deletion** the caller **should** keep track of the created threads and delete them later when necessary. +If you require thread deletion and the provider allows this, the caller **should** keep track of the created threads and delete them later when necessary via the provider's sdk. i.e: OpenAI Assistants Provider ```csharp await assistantClient.DeleteThreadAsync(thread.ConversationId); ``` -### Tool Registration +### 5. Tool Registration #### Semantic Kernel In semantic kernel to expose a function as a tool you must: -1. Decorate the function with `[KernelFunction]` attribute. +1. Decorate the function with a `[KernelFunction]` attribute. 2. Have a `Plugin` class or use the `KernelPluginFactory` to wrap the function. -3. Have a `Kernel` to use add your plugin. +3. Have a `Kernel` to add your plugin to. 4. Pass the `Kernel` to the agent. ```csharp @@ -162,7 +155,7 @@ In agent framework in a single call you can register tools directly in the agent AIAgent agent = chatClient.CreateAIAgent(tools: [AIFunctionFactory.Create(GetWeather)]); ``` -### Agent Non-Streaming Invocation +### 6. Agent Non-Streaming Invocation Key differences can be seen in the method names from `Invoke` to `Run`, return types and parameters `AgentRunOptions`. @@ -179,15 +172,16 @@ await foreach (AgentResponseItem result in agent.InvokeAsync #### Agent Framework -The Non-Streaming returns a single `AgentRunResponse` with the agent response that can contain multiple messages. +The Non-Streaming returns a single `AgentRunResponse` with the agent response that can contain multiple messages. The text result of the run is available in `AgentRunResponse.Text` or `AgentRunResponse.ToString()`. -All intermediate messages that lead up to creating the result is returned in the `AgentRunResponse.Messages` list. +All messages created as part of the response is returned in the `AgentRunResponse.Messages` list. +This may include tool call messages, function results, reasoning updates and final results. ```csharp AgentRunResponse agentResponse = await agent.RunAsync(userInput, thread); ``` -### Agent Streaming Invocation +### 7. Agent Streaming Invocation Key differences in the method names from `Invoke` to `Run`, return types and parameters `AgentRunOptions`. @@ -202,7 +196,7 @@ await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync( #### Agent Framework -Similar streaming API pattern with the key difference being that it `AgentRunResponseUpdate` including more agent related information per update. +Similar streaming API pattern with the key difference being that it returns `AgentRunResponseUpdate` objects including more agent related information per update. All updates produced by any service underlying the AIAgent is returned. The textual result of the agent is available by concatenating the `AgentRunResponse.Text` values. @@ -211,9 +205,12 @@ await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInpu { Console.Write(update); // Update is ToString() friendly } -`` -### Tool Function Signatures +``` + +### 8. Tool Function Signatures + **Problem**: SK plugin methods need `[KernelFunction]` attributes + ```csharp public class MenuPlugin { @@ -223,34 +220,42 @@ public class MenuPlugin ``` **Solution**: AF can use methods directly without attributes + ```csharp public class MenuTools { - [Description("Get menu items")] // Only Description needed + [Description("Get menu items")] // Optional description public static MenuItem[] GetMenu() => ...; } ``` -### Options Configuration +### 9. Options Configuration + **Problem**: Complex options setup in SK + ```csharp OpenAIPromptExecutionSettings settings = new() { MaxTokens = 1000 }; AgentInvokeOptions options = new() { KernelArguments = new(settings) }; ``` **Solution**: Simplified options in AF + ```csharp ChatClientAgentRunOptions options = new(new() { MaxOutputTokens = 1000 }); ``` -### Dependency Injection +> [!IMPORTANT] +> This example shows passing implementation specific options to a `ChatClientAgent`. Not all `AIAgents` support `ChatClientAgentRunOptions`. +> `ChatClientAgent` is provided to build agents based on underlying inference services, and therefore supports inference options like `MaxOutputTokens`. + +### 10. Dependency Injection #### Semantic Kernel A `Kernel` registration is required in the service container to be able to create an agent as every agent abstractions needs to be initialized with a `Kernel` property. -Semantic Kernel uses `Agent` type as the lower level abstraction for agents. +Semantic Kernel uses the `Agent` type as the base abstraction class for agents. ```csharp services.AddKernel().AddProvider(...); @@ -264,32 +269,47 @@ serviceContainer.AddKeyedSingleton( }); ``` +### 11. **Agent Type Consolidation** + +#### Semantic Kernel + +Semantic kernel provides specific agent classes for various services, e.g. + +- `ChatCompletionAgent` for use with chat-completion-based inference services. +- `OpenAIAssistantAgent` for use with the OpenAI Assistants service. +- `AzureAIAgent` for use with the Azure AI Foundry Agents service. + #### Agent Framework -Agent framework lower level agents abstraction are defined as `AIAgent` type to avoid potential type clashes with other `Agent` types -not necessarily related to AI Agents. +The agent framework supports all the abovementioned services via a single agent type, `ChatClientAgent`. + +`ChatClientAgent` can be used to build agents using any underlying service that provides an SDK implementing the `Microsoft.Extensions.AI.IChatClient` interface. + +#### Agent Framework + +The Agent framework provides the `AIAgent` type as the base abstraction class. ```csharp services.AddKeyedSingleton(() => client.CreateAIAgent(...)); ``` -# Migration Samples +## Migration Samples -This folder contains **separate console application projects** demonstrating how to transition from **Semantic Kernel (SK)** to the new **Agent Framework (AF)**. +This folder contains **separate console application projects** demonstrating how to transition from **Semantic Kernel (SK)** to the new **Agent Framework (AF)**. Each project shows side-by-side comparisons of equivalent functionality in both frameworks and can be run independently. Each sample code contains the following: 1. **SK Agent** (Semantic Kernel before) -2. **AF Agent** (Agent Framework after) +2. **AF Agent** (Agent Framework after) -## Running the samples from Visual Studio +### Running the samples from Visual Studio Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`. You will be prompted for any required environment variables if they are not already set. -## Prerequisites +### Prerequisites Before you begin, ensure you have the following: @@ -298,7 +318,7 @@ Before you begin, ensure you have the following: - For OpenAI samples: OpenAI API key - For OpenAI Assistants samples: OpenAI API key with Assistant API access -## Environment Variables +### Environment Variables Set the appropriate environment variables based on the sample type you want to run: @@ -325,7 +345,7 @@ $env:AF_SHOW_ALL_DEMO_SETTING_VALUES = "Y" If environment variables are not set, the demos will prompt you to enter values interactively. -## Samples +### Samples The migration samples are organized into three categories, each demonstrating different AI service integrations: @@ -370,4 +390,4 @@ dotnet run # Azure OpenAI Assistants Examples cd "AzureOpenAIAssistants\Step01_Basics" dotnet run -``` \ No newline at end of file +```