diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 8edd8bffcd..126efa7272 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -71,6 +71,18 @@ + + + + + + + + + + + + diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/AzureOpenAIResponses_Step01_Basics.csproj b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/AzureOpenAIResponses_Step01_Basics.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/AzureOpenAIResponses_Step01_Basics.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs new file mode 100644 index 0000000000..5801959df3 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step01_Basics/Program.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; + +#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. +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var endpoint = Environment.GetEnvironmentVariable("AZUREOPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZUREOPENAI_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZUREOPENAI_DEPLOYMENT_NAME") ?? "gpt-4o"; +var userInput = "Tell me a joke about a pirate."; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + OpenAIResponseAgent agent = new(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName)) + { + Name = "Joker", + Instructions = "You are good at telling jokes.", + }; + + var agentOptions = new OpenAIResponseAgentInvokeOptions() { ResponseCreationOptions = new() { MaxOutputTokenCount = 1000 } }; + + Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) + { + Console.WriteLine(item.Message); + } + + Console.WriteLine("---"); + await foreach (var item in agent.InvokeStreamingAsync(userInput, thread, agentOptions)) + { + // Thread need to be updated for subsequent calls + thread = item.Thread; + Console.Write(item.Message); + } +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 8000 }); + + var result = await agent.RunAsync(userInput, thread, agentOptions); + Console.WriteLine(result); + + Console.WriteLine("---"); + await foreach (var update in agent.RunStreamingAsync(userInput, thread, agentOptions)) + { + Console.Write(update); + } +} diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/AzureOpenAIResponses_Step02_ReasoningModel.csproj b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/AzureOpenAIResponses_Step02_ReasoningModel.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/AzureOpenAIResponses_Step02_ReasoningModel.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs new file mode 100644 index 0000000000..7900e474ed --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step02_ReasoningModel/Program.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; + +#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. +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var endpoint = Environment.GetEnvironmentVariable("AZUREOPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZUREOPENAI_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZUREOPENAI_DEPLOYMENT_NAME") ?? "o4-mini"; +var userInput = + """ + Instructions: + - Given the React component below, think about it and change it so that nonfiction books have red + text. + - Return only the code in your reply + - Do not include any additional formatting, such as markdown code blocks + - For formatting, use four space tabs, and do not allow any lines of code to + exceed 80 columns + const books = [ + { title: 'Dune', category: 'fiction', id: 1 }, + { title: 'Frankenstein', category: 'fiction', id: 2 }, + { title: 'Moneyball', category: 'nonfiction', id: 3 }, + ]; + export default function BookList() { + const listItems = books.map(book => +
  • + {book.title} +
  • + ); + return ( +
      {listItems}
    + ); + } + """; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + OpenAIResponseAgent agent = new(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName)) + { + Name = "Joker", + Instructions = "You are good at telling jokes.", + }; + + var agentOptions = new OpenAIResponseAgentInvokeOptions() + { + ResponseCreationOptions = new() + { + MaxOutputTokenCount = 8000, + ReasoningOptions = new() + { + ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.High, + ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed + } + } + }; + + Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) + { + foreach (var content in item.Message.Items) + { + // Currently SK Responses Agent doesn't distinguish thinking from non-thinking content in non-streaming mode. + // SK Bugfix WIP: https://github.com/microsoft/semantic-kernel/issues/13046 + if (content is ReasoningContent thinking) + { + Console.Write($"Thinking: \n{thinking}\n---\n"); + } + else if (content is Microsoft.SemanticKernel.TextContent text) + { + Console.Write($"Assistant: {text}"); + } + } + Console.WriteLine(item.Message); + } + + Console.WriteLine("---"); + var userMessage = new ChatMessageContent(AuthorRole.User, userInput); + await foreach (var item in agent.InvokeStreamingAsync(userMessage, thread, agentOptions)) + { + thread = item.Thread; + foreach (var content in item.Message.Items) + { + // Currently SK Agent doesn't output thinking in streaming mode. + // SK Bugfix WIP: https://github.com/microsoft/semantic-kernel/issues/13046 + if (content is StreamingReasoningContent thinking) + { + Console.WriteLine($"Thinking: [{thinking}]"); + continue; + } + + if (content is StreamingTextContent text) + { + Console.WriteLine($"Response: [{text}]"); + } + } + } +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() + { + MaxOutputTokens = 8000, + // Microsoft.Extensions.AI currently does not have an abstraction for reasoning-effort, + // we need to break glass using the RawRepresentationFactory. + RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions() + { + ReasoningOptions = new() + { + ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.High, + ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed + } + } + }); + + var result = await agent.RunAsync(userInput, thread, agentOptions); + + // Retrieve the thinking as a full text block requires flattening multiple TextReasoningContents from multiple messages content lists. + string assistantThinking = string.Join("\n", result.Messages + .SelectMany(m => m.Contents) + .OfType() + .Select(trc => trc.Text)); + + var assistantText = result.Text; + Console.WriteLine($"Thinking: \n{assistantThinking}\n---\n"); + Console.WriteLine($"Assistant: \n{assistantText}\n---\n"); + + Console.WriteLine("---"); + await foreach (var update in agent.RunStreamingAsync(userInput, thread, agentOptions)) + { + var thinkingContents = update.Contents + .OfType() + .Select(trc => trc.Text) + .ToList(); + + if (thinkingContents.Count != 0) + { + Console.WriteLine($"Thinking: [{string.Join("\n", thinkingContents)}]"); + continue; + } + + Console.WriteLine($"Response: [{update.Text}]"); + } +} diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/AzureOpenAIResponses_Step03_ToolCall.csproj b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/AzureOpenAIResponses_Step03_ToolCall.csproj new file mode 100644 index 0000000000..dcf6e25469 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/AzureOpenAIResponses_Step03_ToolCall.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707;CA1050;CA1052 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/Program.cs new file mode 100644 index 0000000000..9c9ab8995c --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step03_ToolCall/Program.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var endpoint = Environment.GetEnvironmentVariable("AZUREOPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZUREOPENAI_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZUREOPENAI_DEPLOYMENT_NAME") ?? "gpt-4o"; +var userInput = "What is the weather like in Amsterdam?"; + +Console.WriteLine($"User Input: {userInput}"); + +[KernelFunction] +[Description("Get the weather for a given location.")] +static string GetWeather([Description("The location to get the weather for.")] string location) + => $"The weather in {location} is cloudy with a high of 15°C."; + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + OpenAIResponseAgent agent = new(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName)); + + // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). + agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("KernelPluginName", [KernelFunctionFactory.CreateFromMethod(GetWeather)])); + + Console.WriteLine("\n=== SK Agent Response ===\n"); + + await foreach (ChatMessageContent responseItem in agent.InvokeAsync(userInput)) + { + if (!string.IsNullOrWhiteSpace(responseItem.Content)) + { + Console.WriteLine(responseItem); + } + } +} + +async Task AFAgent() +{ + var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]); + + Console.WriteLine("\n=== AF Agent Response ===\n"); + + var result = await agent.RunAsync(userInput); + Console.WriteLine(result); +} diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/AzureOpenAIResponses_Step04_DependencyInjection.csproj b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/AzureOpenAIResponses_Step04_DependencyInjection.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/AzureOpenAIResponses_Step04_DependencyInjection.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/Program.cs new file mode 100644 index 0000000000..53e65b4c3b --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureOpenAIResponses/Step04_DependencyInjection/Program.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var endpoint = Environment.GetEnvironmentVariable("AZUREOPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZUREOPENAI_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZUREOPENAI_DEPLOYMENT_NAME") ?? "gpt-4o"; +var userInput = "Tell me a joke about a pirate."; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient((sp) + => new OpenAIResponseAgent(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetOpenAIResponseClient(deploymentName)) + { + Name = "Joker", + Instructions = "You are good at telling jokes." + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var result = await agent.InvokeAsync(userInput).FirstAsync(); + Console.WriteLine(result.Message); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient((sp) => new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes.")); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var result = await agent.RunAsync(userInput); + Console.WriteLine(result); +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/OpenAIResponses_Step01_Basics.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/OpenAIResponses_Step01_Basics.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/OpenAIResponses_Step01_Basics.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs new file mode 100644 index 0000000000..dd2147c67c --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step01_Basics/Program.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; + +#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. +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); +var modelId = System.Environment.GetEnvironmentVariable("OPENAI_MODELID") ?? "gpt-4o"; +var userInput = "Tell me a joke about a pirate."; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + OpenAIResponseAgent agent = new(new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId)) + { + Name = "Joker", + Instructions = "You are good at telling jokes.", + }; + + var agentOptions = new OpenAIResponseAgentInvokeOptions() { ResponseCreationOptions = new() { MaxOutputTokenCount = 1000 } }; + + Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) + { + Console.WriteLine(item.Message); + } + + Console.WriteLine("---"); + await foreach (var item in agent.InvokeStreamingAsync(userInput, thread, agentOptions)) + { + // Thread need to be updated for subsequent calls + thread = item.Thread; + Console.Write(item.Message); + } +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var agent = new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 8000 }); + + var result = await agent.RunAsync(userInput, thread, agentOptions); + Console.WriteLine(result); + + Console.WriteLine("---"); + await foreach (var update in agent.RunStreamingAsync(userInput, thread, agentOptions)) + { + Console.Write(update); + } +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/OpenAIResponses_Step02_ReasoningModel.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/OpenAIResponses_Step02_ReasoningModel.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/OpenAIResponses_Step02_ReasoningModel.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs new file mode 100644 index 0000000000..13267811e0 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step02_ReasoningModel/Program.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; + +#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. +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); +var modelId = System.Environment.GetEnvironmentVariable("OPENAI_MODELID") ?? "o4-mini"; +var userInput = + """ + Instructions: + - Given the React component below, think about it and change it so that nonfiction books have red + text. + - Return only the code in your reply + - Do not include any additional formatting, such as markdown code blocks + - For formatting, use four space tabs, and do not allow any lines of code to + exceed 80 columns + const books = [ + { title: 'Dune', category: 'fiction', id: 1 }, + { title: 'Frankenstein', category: 'fiction', id: 2 }, + { title: 'Moneyball', category: 'nonfiction', id: 3 }, + ]; + export default function BookList() { + const listItems = books.map(book => +
  • + {book.title} +
  • + ); + return ( +
      {listItems}
    + ); + } + """; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + OpenAIResponseAgent agent = new(new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId)) + { + Name = "Joker", + Instructions = "You are good at telling jokes.", + }; + + var agentOptions = new OpenAIResponseAgentInvokeOptions() + { + ResponseCreationOptions = new() + { + MaxOutputTokenCount = 8000, + ReasoningOptions = new() + { + ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.High, + ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed + } + } + }; + + Microsoft.SemanticKernel.Agents.AgentThread? thread = null; + await foreach (var item in agent.InvokeAsync(userInput, thread, agentOptions)) + { + foreach (var content in item.Message.Items) + { + // Currently SK Responses Agent doesn't distinguish thinking from non-thinking content in non-streaming mode. + // SK Bugfix WIP: https://github.com/microsoft/semantic-kernel/issues/13046 + if (content is ReasoningContent thinking) + { + Console.Write($"Thinking: \n{thinking}\n---\n"); + } + else if (content is Microsoft.SemanticKernel.TextContent text) + { + Console.Write($"Assistant: {text}"); + } + } + Console.WriteLine(item.Message); + } + + Console.WriteLine("---"); + var userMessage = new ChatMessageContent(AuthorRole.User, userInput); + await foreach (var item in agent.InvokeStreamingAsync(userMessage, thread, agentOptions)) + { + thread = item.Thread; + foreach (var content in item.Message.Items) + { + // Currently SK Agent doesn't output thinking in streaming mode. + // SK Bugfix WIP: https://github.com/microsoft/semantic-kernel/issues/13046 + if (content is StreamingReasoningContent thinking) + { + Console.WriteLine($"Thinking: [{thinking}]"); + continue; + } + + if (content is StreamingTextContent text) + { + Console.WriteLine($"Response: [{text}]"); + } + } + } +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var agent = new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() + { + MaxOutputTokens = 8000, + // Microsoft.Extensions.AI currently does not have an abstraction for reasoning-effort, + // we need to break glass using the RawRepresentationFactory. + RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions() + { + ReasoningOptions = new() + { + ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.High, + ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed + } + } + }); + + var result = await agent.RunAsync(userInput, thread, agentOptions); + + // Retrieve the thinking as a full text block requires flattening multiple TextReasoningContents from multiple messages content lists. + string assistantThinking = string.Join("\n", result.Messages + .SelectMany(m => m.Contents) + .OfType() + .Select(trc => trc.Text)); + + var assistantText = result.Text; + Console.WriteLine($"Thinking: \n{assistantThinking}\n---\n"); + Console.WriteLine($"Assistant: \n{assistantText}\n---\n"); + + Console.WriteLine("---"); + await foreach (var update in agent.RunStreamingAsync(userInput, thread, agentOptions)) + { + var thinkingContents = update.Contents + .OfType() + .Select(trc => trc.Text) + .ToList(); + + if (thinkingContents.Count != 0) + { + Console.WriteLine($"Thinking: [{string.Join("\n", thinkingContents)}]"); + continue; + } + + Console.WriteLine($"Response: [{update.Text}]"); + } +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step03_ToolCall/OpenAIResponses_Step03_ToolCall.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step03_ToolCall/OpenAIResponses_Step03_ToolCall.csproj new file mode 100644 index 0000000000..dcf6e25469 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step03_ToolCall/OpenAIResponses_Step03_ToolCall.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707;CA1050;CA1052 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step03_ToolCall/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step03_ToolCall/Program.cs new file mode 100644 index 0000000000..c7de76df78 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step03_ToolCall/Program.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); +var modelId = System.Environment.GetEnvironmentVariable("OPENAI_MODELID") ?? "gpt-4o"; +var userInput = "What is the weather like in Amsterdam?"; + +Console.WriteLine($"User Input: {userInput}"); + +[KernelFunction] +[Description("Get the weather for a given location.")] +static string GetWeather([Description("The location to get the weather for.")] string location) + => $"The weather in {location} is cloudy with a high of 15°C."; + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + var builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey); + + OpenAIResponseAgent agent = new(new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId)); + + // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). + agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("KernelPluginName", [KernelFunctionFactory.CreateFromMethod(GetWeather)])); + + Console.WriteLine("\n=== SK Agent Response ===\n"); + + await foreach (ChatMessageContent responseItem in agent.InvokeAsync(userInput)) + { + if (!string.IsNullOrWhiteSpace(responseItem.Content)) + { + Console.WriteLine(responseItem); + } + } +} + +async Task AFAgent() +{ + var agent = new OpenAIClient(apiKey).GetChatClient(modelId).CreateAIAgent( + instructions: "You are a helpful assistant", + tools: [AIFunctionFactory.Create(GetWeather)]); + + Console.WriteLine("\n=== AF Agent Response ===\n"); + + var result = await agent.RunAsync(userInput); + Console.WriteLine(result); +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step04_DependencyInjection/OpenAIResponses_Step04_DependencyInjection.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step04_DependencyInjection/OpenAIResponses_Step04_DependencyInjection.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step04_DependencyInjection/OpenAIResponses_Step04_DependencyInjection.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step04_DependencyInjection/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step04_DependencyInjection/Program.cs new file mode 100644 index 0000000000..36f38a5fe9 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIResponses/Step04_DependencyInjection/Program.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; + +#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); +var modelId = System.Environment.GetEnvironmentVariable("OPENAI_MODELID") ?? "gpt-4o"; +var userInput = "Tell me a joke about a pirate."; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient((sp) + => new OpenAIResponseAgent(new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId)) + { + Name = "Joker", + Instructions = "You are good at telling jokes." + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var result = await agent.InvokeAsync(userInput).FirstAsync(); + Console.WriteLine(result.Message); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient((sp) => new OpenAIClient(apiKey) + .GetChatClient(modelId) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes.")); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var result = await agent.RunAsync(userInput); + Console.WriteLine(result); +} diff --git a/dotnet/samples/SemanticKernelMigration/README.md b/dotnet/samples/SemanticKernelMigration/README.md index 9b253aa1c1..92c635d49a 100644 --- a/dotnet/samples/SemanticKernelMigration/README.md +++ b/dotnet/samples/SemanticKernelMigration/README.md @@ -346,6 +346,10 @@ To run any migration sample, navigate to the desired sample directory: cd "AzureAIFoundry\Step01_Basics" dotnet run +# Azure OpenAI Examples +cd "AzureOpenAI\Step01_Basics" +dotnet run + # OpenAI Examples cd "OpenAI\Step01_Basics" dotnet run @@ -354,6 +358,9 @@ dotnet run cd "OpenAIAssistants\Step01_Basics" dotnet run +# OpenAI Responses Examples +cd "OpenAIResponses\Step01_Basics" + # Azure OpenAI Examples cd "AzureOpenAI\Step01_Basics" dotnet run diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIResponsesChatClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIResponsesChatClient.cs index 2e6051231a..bd849ad320 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIResponsesChatClient.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIResponsesChatClient.cs @@ -11,6 +11,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; @@ -26,6 +27,9 @@ namespace Microsoft.Extensions.AI; /// Represents an for an . internal sealed class NewOpenAIResponsesChatClient : IChatClient { + /// Type info for serializing and deserializing arbitrary JSON objects. + private static readonly JsonTypeInfo s_jsonTypeInfo = AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)); + /// Metadata about the client. private readonly ChatClientMetadata _metadata; @@ -324,8 +328,24 @@ internal sealed class NewOpenAIResponsesChatClient : IChatClient break; default: + { + // Capture streaming thinking contents + if (streamingUpdate.GetType().Name == "InternalResponseReasoningSummaryTextDeltaEvent") + { + var updateJson = JsonSerializer.Deserialize( + JsonSerializer.Serialize(streamingUpdate, s_jsonTypeInfo), + OpenAIJsonContext2.Default.JsonElement); + + if (updateJson.TryGetProperty("delta", out var deltaProperty)) + { + yield return CreateUpdate(new TextReasoningContent(deltaProperty.GetString())); + break; + } + } + yield return CreateUpdate(); break; + } } } }