From dac17a8f4877d95c8b4959127ac3b7b9f9d377e1 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:29:03 +0100 Subject: [PATCH] .NET: Adding Semantic Kernel - Migration Samples (#499) * Guidance * Guidance * WIP Migration Preps * Move to single file projects * Update guidance code and final adjustments to ensure all feature compatibility * Move from demos to samples * Address format * Address chat client pipeline order * Update project naming * Revisition on README * Remove unused ctor * Address feedback * Address feedback * Address merge conflict fix * Address SK versioning * Address folder naming * Address feedback --- dotnet/Directory.Packages.props | 8 +- dotnet/agent-framework-dotnet.slnx | 20 + dotnet/demos/Directory.Build.props | 2 +- dotnet/samples/.gitignore | 1 + dotnet/samples/GettingStarted/AgentSample.cs | 8 +- .../AzureAIFoundry_Step01_Basics.csproj | 25 ++ .../AzureAIFoundry/Step01_Basics/Program.cs | 76 ++++ .../AzureAIFoundry_Step02_ToolCall.csproj | 24 ++ .../AzureAIFoundry/Step02_ToolCall/Program.cs | 85 +++++ ...IFoundry_Step03_DependencyInjection.csproj | 24 ++ .../Step03_DependencyInjection/Program.cs | 100 +++++ ...ureAIFoundry_Step04_CodeInterpreter.csproj | 24 ++ .../Step04_CodeInterpreter/Program.cs | 121 ++++++ .../Step01_Basics/OpenAI_Step01_Basics.csproj | 24 ++ .../OpenAI/Step01_Basics/Program.cs | 64 ++++ .../OpenAI_Step02_ToolCall.csproj | 24 ++ .../OpenAI/Step02_ToolCall/Program.cs | 53 +++ .../OpenAI_Step03_DependencyInjection.csproj | 24 ++ .../Step03_DependencyInjection/Program.cs | 53 +++ .../OpenAIAssistants_Step01_Basics.csproj | 24 ++ .../OpenAIAssistants/Step01_Basics/Program.cs | 80 ++++ .../OpenAIAssistants_Step02_ToolCall.csproj | 24 ++ .../Step02_ToolCall/Program.cs | 95 +++++ ...sistants_Step03_DependencyInjection.csproj | 24 ++ .../Step03_DependencyInjection/Program.cs | 97 +++++ ...AIAssistants_Step04_CodeInterpreter.csproj | 24 ++ .../Step04_CodeInterpreter/Program.cs | 127 +++++++ .../samples/SemanticKernelMigration/README.md | 357 ++++++++++++++++++ ...rosoft.Extensions.AI.Agents.AzureAI.csproj | 1 + .../NewPersistentAgentsChatClient.cs | 256 ++++++++++--- .../PersistentAgentResponseExtensions.cs | 2 +- .../PersistentAgentsClientExtensions.cs | 90 +++++ ...crosoft.Extensions.AI.Agents.OpenAI.csproj | 1 + .../NewOpenAIAssistantChatClient.cs | 112 ++---- .../OpenAIAssistantClientExtensions.cs | 18 +- .../ChatCompletion/ChatClientAgent.cs | 4 +- .../ChatCompletion/ChatClientExtensions.cs | 15 +- 37 files changed, 1973 insertions(+), 138 deletions(-) create mode 100644 dotnet/samples/.gitignore create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/AzureAIFoundry_Step02_ToolCall.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/AzureAIFoundry_Step03_DependencyInjection.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/AzureAIFoundry_Step04_CodeInterpreter.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/OpenAI_Step01_Basics.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/OpenAI_Step02_ToolCall.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAI/Step03_DependencyInjection/OpenAI_Step03_DependencyInjection.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAI/Step03_DependencyInjection/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step01_Basics/OpenAIAssistants_Step01_Basics.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step01_Basics/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/OpenAIAssistants_Step02_ToolCall.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/OpenAIAssistants_Step03_DependencyInjection.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/OpenAIAssistants_Step04_CodeInterpreter.csproj create mode 100644 dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/Program.cs create mode 100644 dotnet/samples/SemanticKernelMigration/README.md diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 5f1f297c92..df6a36cc41 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -17,7 +17,7 @@ - + @@ -67,7 +67,11 @@ - + + + + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 1722d656c4..d14fe62675 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -8,6 +8,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/demos/Directory.Build.props b/dotnet/demos/Directory.Build.props index d69fd22796..cfcc22dab6 100644 --- a/dotnet/demos/Directory.Build.props +++ b/dotnet/demos/Directory.Build.props @@ -14,7 +14,7 @@ - + diff --git a/dotnet/samples/.gitignore b/dotnet/samples/.gitignore new file mode 100644 index 0000000000..8392c905c6 --- /dev/null +++ b/dotnet/samples/.gitignore @@ -0,0 +1 @@ +launchSettings.json \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/AgentSample.cs b/dotnet/samples/GettingStarted/AgentSample.cs index 5c23370b85..004f1bf2e0 100644 --- a/dotnet/samples/GettingStarted/AgentSample.cs +++ b/dotnet/samples/GettingStarted/AgentSample.cs @@ -113,11 +113,11 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output) => new OpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .AsNewIChatClient(); - private NewPersistentAgentsChatClient GetAzureAIAgentPersistentClient(ChatClientAgentOptions options) - => new(new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()), options.Id!); + private IChatClient GetAzureAIAgentPersistentClient(ChatClientAgentOptions options) + => new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()).AsNewIChatClient(options.Id!); - private NewOpenAIAssistantChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options) - => new(new AssistantClient(TestConfiguration.OpenAI.ApiKey), options.Id!); + private IChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options) + => new AssistantClient(TestConfiguration.OpenAI.ApiKey).AsNewIChatClient(options.Id!); #endregion diff --git a/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj new file mode 100644 index 0000000000..f0655e396b --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + true + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/Program.cs new file mode 100644 index 0000000000..3d0dac58ba --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/Program.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.AzureAI; + +#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. + +var azureEndpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_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 azureAgentClient = AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential()); + + PersistentAgent definition = await azureAgentClient.Administration.CreateAgentAsync( + deploymentName, + name: "GenerateStory", + instructions: "You are good at telling jokes."); + + AzureAIAgent agent = new(definition, azureAgentClient); + + var thread = new AzureAIAgentThread(azureAgentClient); + + AzureAIAgentInvokeOptions options = new() { MaxPromptTokens = 1000 }; + var result = await agent.InvokeAsync(userInput, thread, options).FirstAsync(); + Console.WriteLine(result.Message); + + Console.WriteLine("---"); + await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync(userInput, thread)) + { + Console.Write(update); + } + + // Clean up + await thread.DeleteAsync(); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var azureAgentClient = new PersistentAgentsClient(azureEndpoint, new AzureCliCredential()); + + var agent = await azureAgentClient.CreateAIAgentAsync( + deploymentName, + name: "GenerateStory", + instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 1000 }); + + 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); + } + + // Clean up + await azureAgentClient.Threads.DeleteThreadAsync(thread.ConversationId); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/AzureAIFoundry_Step02_ToolCall.csproj b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/AzureAIFoundry_Step02_ToolCall.csproj new file mode 100644 index 0000000000..5fb1669c6d --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/AzureAIFoundry_Step02_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/AzureAIFoundry/Step02_ToolCall/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/Program.cs new file mode 100644 index 0000000000..9caaa2dd2b --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step02_ToolCall/Program.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.AzureAI; + +#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. + +var azureEndpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_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() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + var azureAgentClient = AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential()); + + PersistentAgent definition = await azureAgentClient.Administration.CreateAgentAsync(deploymentName, instructions: "You are a helpful assistant"); + + AzureAIAgent agent = new(definition, azureAgentClient) + { + Kernel = Kernel.CreateBuilder().Build(), + Name = "Host", + Instructions = "You are a helpful assistant", + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + + var thread = new AzureAIAgentThread(azureAgentClient); + + // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). + agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("KernelPluginName", [KernelFunctionFactory.CreateFromMethod(GetWeather)])); + + var result = await agent.InvokeAsync(userInput).FirstAsync(); + Console.WriteLine(result.Message); + + Console.WriteLine("---"); + await foreach (ChatMessageContent update in agent.InvokeAsync(userInput, thread)) + { + Console.Write(update); + } + + // Clean up + await thread.DeleteAsync(); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var azureAgentClient = new PersistentAgentsClient(azureEndpoint, new AzureCliCredential()); + + var agent = await azureAgentClient.CreateAIAgentAsync(deploymentName, instructions: "Answer questions about the menu"); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { Tools = [AIFunctionFactory.Create(GetWeather)] }); + + 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); + } + + // Clean up + await azureAgentClient.Threads.DeleteThreadAsync(thread.ConversationId); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/AzureAIFoundry_Step03_DependencyInjection.csproj b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/AzureAIFoundry_Step03_DependencyInjection.csproj new file mode 100644 index 0000000000..3e22c21d32 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/AzureAIFoundry_Step03_DependencyInjection.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + $(NoWarn);CA1812;RCS1102;CA1707 + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/Program.cs new file mode 100644 index 0000000000..e944c02091 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step03_DependencyInjection/Program.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.AzureAI; + +#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. + +var azureEndpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_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.AddSingleton((sp) => AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential())); + serviceCollection.AddTransient((sp) => + { + var azureAgentClient = sp.GetRequiredService(); + + Console.Write("Creating agent in the cloud..."); + + PersistentAgent definition = azureAgentClient.Administration + .CreateAgent(deploymentName, + name: "GenerateStory", + instructions: "You are good at telling jokes."); + + Console.Write("Done\n"); + + return new(definition, azureAgentClient); + }); + serviceCollection.AddKernel(); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var thread = new AzureAIAgentThread(agent.Client); + + var result = await agent.InvokeAsync(userInput).FirstAsync(); + Console.WriteLine(result.Message); + + Console.WriteLine("---"); + await foreach (ChatMessageContent update in agent.InvokeAsync(userInput, thread)) + { + Console.Write(update); + } + + // Clean up + await thread.DeleteAsync(); + await agent.Client.Administration.DeleteAgentAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton((sp) => AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential())); + serviceCollection.AddTransient((sp) => + { + var azureAgentClient = sp.GetRequiredService(); + + var aiAgent = azureAgentClient.CreateAIAgent( + deploymentName, + name: "GenerateStory", + instructions: "You are good at telling jokes."); + + return aiAgent; + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var thread = agent.GetNewThread(); + + var result = await agent.RunAsync(userInput, thread); + Console.WriteLine(result); + + Console.WriteLine("---"); + await foreach (var update in agent.RunStreamingAsync(userInput, thread)) + { + Console.Write(update); + } + + // Clean up + var azureAgentClient = serviceProvider.GetRequiredService(); + await azureAgentClient.Threads.DeleteThreadAsync(thread.ConversationId); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/AzureAIFoundry_Step04_CodeInterpreter.csproj b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/AzureAIFoundry_Step04_CodeInterpreter.csproj new file mode 100644 index 0000000000..a71ffa2f51 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/AzureAIFoundry_Step04_CodeInterpreter.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/AzureAIFoundry/Step04_CodeInterpreter/Program.cs b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/Program.cs new file mode 100644 index 0000000000..436f6d2d46 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/AzureAIFoundry/Step04_CodeInterpreter/Program.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.AzureAI; + +#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 CS8321 // Local function is declared but never used + +var azureEndpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o"; +var userInput = "Create a python code file using the code interpreter tool with a code ready to determine the values in the Fibonacci sequence that are less then the value of 101"; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + var azureAgentClient = AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential()); + + PersistentAgent definition = await azureAgentClient.Administration.CreateAgentAsync(deploymentName, tools: [new CodeInterpreterToolDefinition()]); + + AzureAIAgent agent = new(definition, azureAgentClient); + var thread = new AzureAIAgentThread(azureAgentClient); + + // SK Azure AI Agent provides the code interpreter content and the assistant message as different contents in the call iteration. + await foreach (var content in agent.InvokeAsync(userInput, thread)) + { + if (!string.IsNullOrWhiteSpace(content.Message.Content)) + { + bool isCode = content.Message.Metadata?.ContainsKey(AzureAIAgent.CodeInterpreterMetadataKey) ?? false; + Console.WriteLine($"\n# {content.Message.Role}{(isCode ? "\n# Generated Code:\n" : ":")}{content.Message.Content}"); + } + + // Check for the citations + foreach (var item in content.Message.Items) + { + // Process each item in the message +#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. + if (item is AnnotationContent annotation) + { + if (annotation.Kind != AnnotationKind.UrlCitation) + { + Console.WriteLine($" [{item.GetType().Name}] {annotation.Label}: File #{annotation.ReferenceId}"); + } + } + else if (item is FileReferenceContent fileReference) + { + Console.WriteLine($" [{item.GetType().Name}] File #{fileReference.FileId}"); + } + } + } +#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. + + // Clean up + await thread.DeleteAsync(); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var azureAgentClient = new PersistentAgentsClient(azureEndpoint, new AzureCliCredential()); + var agent = await azureAgentClient.CreateAIAgentAsync(deploymentName, tools: [new CodeInterpreterToolDefinition()]); + var thread = agent.GetNewThread(); + + var result = await agent.RunAsync(userInput, thread); + Console.WriteLine(result); + + // Extracts via breaking glass the code generated by code interpreter tool + var chatResponse = result.RawRepresentation as ChatResponse; + StringBuilder generatedCode = new(); + foreach (object? updateRawRepresentation in chatResponse?.RawRepresentation as IEnumerable ?? []) + { + if (updateRawRepresentation is RunStepDetailsUpdate update && update.CodeInterpreterInput is not null) + { + generatedCode.Append(update.CodeInterpreterInput); + } + } + + if (!string.IsNullOrEmpty(generatedCode.ToString())) + { + Console.WriteLine($"\n# {chatResponse?.Messages[0].Role}:Generated Code:\n{generatedCode}"); + } + + // Update the citations + foreach (var textContent in result.Messages[0].Contents.OfType()) + { + foreach (var annotation in textContent.Annotations ?? []) + { + if (annotation is CitationAnnotation citation) + { + if (citation.Url is null) + { + Console.WriteLine($" [{citation.GetType().Name}] {citation.Snippet}: File #{citation.FileId}"); + } + + foreach (var region in citation.AnnotatedRegions ?? []) + { + if (region is TextSpanAnnotatedRegion textSpanRegion) + { + Console.WriteLine($"\n[TextSpan Region] {textSpanRegion.StartIndex}-{textSpanRegion.EndIndex}"); + } + } + } + } + } + + // Clean up + await azureAgentClient.Threads.DeleteThreadAsync(thread.ConversationId); + await azureAgentClient.Administration.DeleteAgentAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/OpenAI_Step01_Basics.csproj b/dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/OpenAI_Step01_Basics.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/OpenAI_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/OpenAI/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/Program.cs new file mode 100644 index 0000000000..c435d02058 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAI/Step01_Basics/Program.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; + +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 builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey); + + var agent = new ChatCompletionAgent() + { + Kernel = builder.Build(), + Name = "Joker", + Instructions = "You are good at telling jokes.", + }; + + var thread = new ChatHistoryAgentThread(); + var settings = new OpenAIPromptExecutionSettings() { MaxTokens = 1000 }; + var agentOptions = new AgentInvokeOptions() { KernelArguments = new(settings) }; + + await foreach (var result in agent.InvokeAsync(userInput, thread, agentOptions)) + { + Console.WriteLine(result.Message); + } + + Console.WriteLine("---"); + await foreach (var update in agent.InvokeStreamingAsync(userInput, thread, agentOptions)) + { + Console.Write(update.Message); + } +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var agent = new OpenAIClient(apiKey).GetChatClient(modelId) + .CreateAIAgent(name: "Joker", instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 1000 }); + + var result = await agent.RunAsync(userInput, thread, agentOptions); + + Console.WriteLine("---"); + await foreach (var update in agent.RunStreamingAsync(userInput, thread, agentOptions)) + { + Console.Write(update); + } +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/OpenAI_Step02_ToolCall.csproj b/dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/OpenAI_Step02_ToolCall.csproj new file mode 100644 index 0000000000..dcf6e25469 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/OpenAI_Step02_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/OpenAI/Step02_ToolCall/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/Program.cs new file mode 100644 index 0000000000..525e3d1ad4 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAI/Step02_ToolCall/Program.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using OpenAI; + +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); + + ChatCompletionAgent agent = new() + { + Instructions = "You are a helpful assistant", + Kernel = builder.Build(), + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + + // 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"); + + var result = await agent.InvokeAsync(userInput).FirstAsync(); + Console.WriteLine(result.Message); +} + +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/OpenAI/Step03_DependencyInjection/OpenAI_Step03_DependencyInjection.csproj b/dotnet/samples/SemanticKernelMigration/OpenAI/Step03_DependencyInjection/OpenAI_Step03_DependencyInjection.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAI/Step03_DependencyInjection/OpenAI_Step03_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/OpenAI/Step03_DependencyInjection/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAI/Step03_DependencyInjection/Program.cs new file mode 100644 index 0000000000..b778dee67e --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAI/Step03_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; +using Microsoft.SemanticKernel.Agents; +using OpenAI; + +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.AddKernel().AddOpenAIChatClient(modelId, apiKey); + serviceCollection.AddTransient((sp) => new ChatCompletionAgent() + { + Kernel = sp.GetRequiredService(), + 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/OpenAIAssistants/Step01_Basics/OpenAIAssistants_Step01_Basics.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step01_Basics/OpenAIAssistants_Step01_Basics.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step01_Basics/OpenAIAssistants_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/OpenAIAssistants/Step01_Basics/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step01_Basics/Program.cs new file mode 100644 index 0000000000..9d14e0ccbc --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step01_Basics/Program.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. + +#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. + +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; +using OpenAI.Assistants; + +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 builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey); + + var assistantsClient = new AssistantClient(apiKey); + + // Define the assistant + Assistant assistant = await assistantsClient.CreateAssistantAsync(modelId, name: "Joker", instructions: "You are good at telling jokes."); + + // Create the agent + OpenAIAssistantAgent agent = new(assistant, assistantsClient); + + // Create a thread for the agent conversation. + var thread = new OpenAIAssistantAgentThread(assistantsClient); + var settings = new OpenAIPromptExecutionSettings() { MaxTokens = 1000 }; + var agentOptions = new OpenAIAssistantAgentInvokeOptions() { KernelArguments = new(settings) }; + + await foreach (var result in agent.InvokeAsync(userInput, thread, agentOptions)) + { + Console.WriteLine(result.Message); + } + + Console.WriteLine("---"); + await foreach (var update in agent.InvokeStreamingAsync(userInput, thread, agentOptions)) + { + Console.Write(update.Message); + } + + // Clean up + await thread.DeleteAsync(); + await assistantsClient.DeleteAssistantAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var assistantClient = new AssistantClient(apiKey); + + var agent = await assistantClient.CreateAIAgentAsync(modelId, name: "Joker", instructions: "You are good at telling jokes."); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 1000 }); + + 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); + } + + var yes = agent.GetService() is not null; + // Clean up + await assistantClient.DeleteThreadAsync(thread.ConversationId); + await assistantClient.DeleteAssistantAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/OpenAIAssistants_Step02_ToolCall.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/OpenAIAssistants_Step02_ToolCall.csproj new file mode 100644 index 0000000000..dcf6e25469 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/OpenAIAssistants_Step02_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/OpenAIAssistants/Step02_ToolCall/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/Program.cs new file mode 100644 index 0000000000..68203fb178 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step02_ToolCall/Program.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +#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. + +using System.ComponentModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; +using OpenAI.Assistants; + +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?"; + +[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."; + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + var builder = Kernel.CreateBuilder(); + var assistantsClient = new AssistantClient(apiKey); + + Assistant assistant = await assistantsClient.CreateAssistantAsync(modelId, + instructions: "You are a helpful assistant"); + + OpenAIAssistantAgent agent = new(assistant, assistantsClient) + { + Kernel = builder.Build(), + Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() + { + MaxTokens = 1000, + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() + }), + }; + + // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). + agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("KernelPluginName", [KernelFunctionFactory.CreateFromMethod(GetWeather)])); + + // Create a thread for the agent conversation. + var thread = new OpenAIAssistantAgentThread(assistantsClient); + + await foreach (var result in agent.InvokeAsync(userInput, thread)) + { + Console.WriteLine(result.Message); + } + + Console.WriteLine("---"); + await foreach (var update in agent.InvokeStreamingAsync(userInput, thread)) + { + Console.Write(update.Message); + } + + // Clean up + await thread.DeleteAsync(); + await assistantsClient.DeleteAssistantAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var assistantClient = new AssistantClient(apiKey); + + var agent = await assistantClient.CreateAIAgentAsync(modelId, + instructions: "You are a helpful assistant", + tools: [AIFunctionFactory.Create(GetWeather)]); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 1000 }); + + 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); + } + + // Clean up + await assistantClient.DeleteThreadAsync(thread.ConversationId); + await assistantClient.DeleteAssistantAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/OpenAIAssistants_Step03_DependencyInjection.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/OpenAIAssistants_Step03_DependencyInjection.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/OpenAIAssistants_Step03_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/OpenAIAssistants/Step03_DependencyInjection/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/Program.cs new file mode 100644 index 0000000000..2ffac58a8e --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step03_DependencyInjection/Program.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +#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. + +using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; +using OpenAI.Assistants; + +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.AddSingleton((sp) => new AssistantClient(apiKey)); + serviceCollection.AddKernel().AddOpenAIChatClient(modelId, apiKey); + serviceCollection.AddTransient((sp) => + { + var assistantsClient = sp.GetRequiredService(); + + Assistant assistant = assistantsClient.CreateAssistant(modelId, new() { Name = "Joker", Instructions = "You are good at telling jokes." }); + + return new OpenAIAssistantAgent(assistant, assistantsClient); + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + // Create a thread for the agent conversation. + var assistantsClient = serviceProvider.GetRequiredService(); + var thread = new OpenAIAssistantAgentThread(assistantsClient); + var settings = new OpenAIPromptExecutionSettings() { MaxTokens = 1000 }; + var agentOptions = new OpenAIAssistantAgentInvokeOptions() { KernelArguments = new(settings) }; + + await foreach (var result in agent.InvokeAsync(userInput, thread, agentOptions)) + { + Console.WriteLine(result.Message); + } + + Console.WriteLine("---"); + await foreach (var update in agent.InvokeStreamingAsync(userInput, thread, agentOptions)) + { + Console.Write(update.Message); + } + + // Clean up + await thread.DeleteAsync(); + await assistantsClient.DeleteAssistantAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton((sp) => new AssistantClient(apiKey)); + serviceCollection.AddTransient((sp) => + { + var assistantClient = sp.GetRequiredService(); + + var agent = assistantClient.CreateAIAgent(modelId, name: "Joker", instructions: "You are good at telling jokes."); + + return agent; + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredService(); + + var thread = agent.GetNewThread(); + var agentOptions = new ChatClientAgentRunOptions(new() { MaxOutputTokens = 1000 }); + + 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); + } + + // Clean up + var assistantClient = serviceProvider.GetRequiredService(); + await assistantClient.DeleteThreadAsync(thread.ConversationId); + await assistantClient.DeleteAssistantAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/OpenAIAssistants_Step04_CodeInterpreter.csproj b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/OpenAIAssistants_Step04_CodeInterpreter.csproj new file mode 100644 index 0000000000..3f455d079f --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/OpenAIAssistants_Step04_CodeInterpreter.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/OpenAIAssistants/Step04_CodeInterpreter/Program.cs b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/Program.cs new file mode 100644 index 0000000000..62de620c5e --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/OpenAIAssistants/Step04_CodeInterpreter/Program.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; +using OpenAI.Assistants; + +#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. +#pragma warning disable CS8321 // Local function is declared but never used + +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 = "Create a python code file using the code interpreter tool with a code ready to determine the values in the Fibonacci sequence that are less then the value of 101"; + +var assistantsClient = new AssistantClient(apiKey); + +Console.WriteLine($"User Input: {userInput}"); + +await SKAgent(); +await AFAgent(); + +async Task SKAgent() +{ + Console.WriteLine("\n=== SK Agent ===\n"); + + var builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey); + + // Define the assistant + Assistant assistant = await assistantsClient.CreateAssistantAsync(modelId, enableCodeInterpreter: true); + + // Create the agent + OpenAIAssistantAgent agent = new(assistant, assistantsClient); + + // Create a thread for the agent conversation. + var thread = new OpenAIAssistantAgentThread(assistantsClient); + + // Respond to user input + await foreach (var content in agent.InvokeAsync(userInput, thread)) + { + if (!string.IsNullOrWhiteSpace(content.Message.Content)) + { + bool isCode = content.Message.Metadata?.ContainsKey(OpenAIAssistantAgent.CodeInterpreterMetadataKey) ?? false; + Console.WriteLine($"\n# {content.Message.Role}{(isCode ? "\n# Generated Code:\n" : ":")}{content.Message.Content}"); + } + + // Check for the citations + foreach (var item in content.Message.Items) + { + // Process each item in the message +#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. + if (item is AnnotationContent annotation) + { + if (annotation.Kind != AnnotationKind.UrlCitation) + { + Console.WriteLine($" [{item.GetType().Name}] {annotation.Label}: File #{annotation.ReferenceId}"); + } + } + else if (item is FileReferenceContent fileReference) + { + Console.WriteLine($" [{item.GetType().Name}] File #{fileReference.FileId}"); + } + } + } +#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. + + // Clean up + await thread.DeleteAsync(); + await assistantsClient.DeleteAssistantAsync(agent.Id); +} + +async Task AFAgent() +{ + Console.WriteLine("\n=== AF Agent ===\n"); + + var agent = await assistantsClient.CreateAIAgentAsync(modelId, tools: [new HostedCodeInterpreterTool()]); + + var thread = agent.GetNewThread(); + + var result = await agent.RunAsync(userInput, thread); + Console.WriteLine(result); + + // Extracts via breaking glass the code generated by code interpreter tool + var chatResponse = result.RawRepresentation as ChatResponse; + StringBuilder generatedCode = new(); + foreach (object? updateRawRepresentation in chatResponse?.RawRepresentation as IEnumerable ?? []) + { + if (updateRawRepresentation is RunStepDetailsUpdate update && update.CodeInterpreterInput is not null) + { + generatedCode.Append(update.CodeInterpreterInput); + } + } + + if (!string.IsNullOrEmpty(generatedCode.ToString())) + { + Console.WriteLine($"\n# {chatResponse?.Messages[0].Role}:Generated Code:\n{generatedCode}"); + } + + // Check for the citations + foreach (var textContent in result.Messages[0].Contents.OfType()) + { + foreach (var annotation in textContent.Annotations ?? []) + { + if (annotation is CitationAnnotation citation) + { + if (citation.Url is null) + { + Console.WriteLine($" [{citation.GetType().Name}] {citation.Snippet}: File #{citation.FileId}"); + } + + foreach (var region in citation.AnnotatedRegions ?? []) + { + if (region is TextSpanAnnotatedRegion textSpanRegion) + { + Console.WriteLine($"\n[TextSpan Region] {textSpanRegion.StartIndex}-{textSpanRegion.EndIndex}"); + } + } + } + } + } + + // Clean up + await assistantsClient.DeleteThreadAsync(thread.ConversationId); + await assistantsClient.DeleteAssistantAsync(agent.Id); +} diff --git a/dotnet/samples/SemanticKernelMigration/README.md b/dotnet/samples/SemanticKernelMigration/README.md new file mode 100644 index 0000000000..b49bc4bad7 --- /dev/null +++ b/dotnet/samples/SemanticKernelMigration/README.md @@ -0,0 +1,357 @@ +# Semantic Kernel to Agent Framework Migration Guide + +## What's Changed? +- **Namespace Updates**: From `Microsoft.SemanticKernel.Agents` to `Microsoft.Extensions.AI.Agents` +- **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 +- **Dependency Injection**: Simplified service registration patterns +- **Invocation Patterns**: Streamlined options and result handling + +## Benefits of Migration +- **Simplified API**: Reduced complexity and boilerplate code +- **Better Performance**: Optimized object creation and memory usage +- **Unified Interface**: Consistent patterns across different AI providers +- **Enhanced Developer Experience**: More intuitive and discoverable APIs + +## Key Changes + +### 1. Namespace Updates + +#### Semantic Kernel + +```csharp +using Microsoft.SemanticKernel; +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 + +```csharp +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +``` + +### 2. Agent Creation Simplification + +#### Semantic Kernel + +Every agent in Semantic Kernel depends on a `Kernel` instance and will have +an empty `Kernel` if not provided. + +```csharp + Kernel kernel = Kernel + .AddOpenAIChatClient(modelId, apiKey) + .Build(); + + ChatCompletionAgent agent = new() { Instructions = ParrotInstructions, Kernel = kernel }; +``` + +Azure AI Foundry requires a strong setup before creating an agent + +```csharp +PersistentAgentsClient azureAgentClient = AzureAIAgent.CreateAgentsClient(azureEndpoint, new AzureCliCredential()); + +PersistentAgent definition = await azureAgentClient.Administration.CreateAgentAsync( + deploymentName, + instructions: ParrotInstructions); + +AzureAIAgent agent = new(definition, azureAgentClient); + ``` + +#### Agent Framework + +Agent creation in Agent Framework is made simpler with extensions provided by all main providers. + +```csharp +AIAgent openAIAgent = chatClient.CreateAIAgent(instructions: ParrotInstructions); +AIAgent azureFoundryAgent = persistentAgentsClient.CreateAIAgent(instructions: ParrotInstructions); +AIAgent openAIAssistantAgent = assistantClient.CreateAIAgent(instructions: ParrotInstructions); +``` + +Additionally for hosted agent providers you can also use the `GetAIAgent` to retrieve an agent from an existing hosted agent. + +```csharp +AIAgent azureFoundryAgent = await persistentAgentsClient.GetAIAgentAsync(agentId); +``` + + +### Agent Thread Creation + +#### Semantic Kernel + +The caller has to know the thread type and create it manually. + +```csharp +// Create a thread for the agent conversation. +AgentThread thread = new OpenAIAssistantAgentThread(this.AssistantClient); +AgentThread thread = new AzureAIAgentThread(this.Client); +AgentThread thread = new OpenAIResponseAgentThread(this.Client); +``` + +#### Agent Framework + +The agent is responsible for creating the thread. + +```csharp +// New +AgentThread thread = agent.GetNewThread(); +``` + +### Hosted Agent Thread Cleanup + +This case applies exclusively to a few AI providers that still provide hosted threads. + +#### Semantic Kernel + +Threads have a `self` deletion method + +i.e: OpenAI Assistants Provider +```csharp +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 + + + +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. + +i.e: OpenAI Assistants Provider +```csharp +await assistantClient.DeleteThreadAsync(thread.ConversationId); +``` + +### Tool Registration + +#### Semantic Kernel + +In semantic kernel to expose a function as a tool you must: + +1. Decorate the function with `[KernelFunction]` attribute. +2. Have a `Plugin` class or use the `KernelPluginFactory` to wrap the function. +3. Have a `Kernel` to use add your plugin. +4. Pass the `Kernel` to the agent. + +```csharp +KernelFunction function = KernelFunctionFactory.CreateFromMethod(GetWeather); +KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("KernelPluginName", [function]); +Kernel kernel = ... // Create kernel +kernel.Plugins.Add(plugin); + +ChatCompletionAgent agent = new() { Kernel = kernel, ... }; +``` + +#### Agent Framework + +In agent framework in a single call you can register tools directly in the agent creation process. + +```csharp +AIAgent agent = chatClient.CreateAIAgent(tools: [AIFunctionFactory.Create(GetWeather)]); +``` + +### Agent Non-Streaming Invocation + +Key differences can be seen in the method names from `Invoke` to `Run`, return types and parameters `AgentRunOptions`. + +#### Semantic Kernel + +The Non-Streaming uses a streaming pattern `IAsyncEnumerable>` for returning multiple agent messages. + +```csharp +await foreach (AgentResponseItem result in agent.InvokeAsync(userInput, thread, agentOptions)) +{ + Console.WriteLine(result.Message); +} +``` + +#### Agent Framework + +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. + +```csharp +AgentRunResponse agentResponse = await agent.RunAsync(userInput, thread); +``` + +### Agent Streaming Invocation + +Key differences in the method names from `Invoke` to `Run`, return types and parameters `AgentRunOptions`. + +#### Semantic Kernel + +```csharp +await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync(userInput, thread)) +{ + Console.Write(update); +} +``` + +#### Agent Framework + +Similar streaming API pattern with the key difference being that it `AgentRunResponseUpdate` 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. + +```csharp +await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInput, thread)) +{ + Console.Write(update); // Update is ToString() friendly +} +`` +### Tool Function Signatures +**Problem**: SK plugin methods need `[KernelFunction]` attributes +```csharp +public class MenuPlugin +{ + [KernelFunction] // Required for SK + public static MenuItem[] GetMenu() => ...; +} +``` + +**Solution**: AF can use methods directly without attributes +```csharp +public class MenuTools +{ + [Description("Get menu items")] // Only Description needed + public static MenuItem[] GetMenu() => ...; +} +``` + +### 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 + +#### 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. + +```csharp +services.AddKernel().AddProvider(...); +serviceContainer.AddKeyedSingleton( + TutorName, + (sp, key) => + new ChatCompletionAgent() + { + // Passing the kernel is required + Kernel = sp.GetRequiredService(), + }); +``` + +#### 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. + +```csharp +services.AddKeyedSingleton(() => client.CreateAIAgent(...)); +``` + +# Migration Samples + +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) + +## 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 + +Before you begin, ensure you have the following: + +- [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download) +- For Azure AI Foundry samples: Azure OpenAI service endpoint and deployment configured +- For OpenAI samples: OpenAI API key +- For OpenAI Assistants samples: OpenAI API key with Assistant API access + +## Environment Variables + +Set the appropriate environment variables based on the sample type you want to run: + +**For Azure AI Foundry projects:** +```powershell +$env:AZURE_FOUNDRY_PROJECT_ENDPOINT = "https://-resource.services.ai.azure.com/api/projects/" +``` + +**For OpenAI and OpenAI Assistants projects:** +```powershell +$env:OPENAI_API_KEY = "sk-..." +``` + +**Optional debug mode:** +```powershell +$env:AF_SHOW_ALL_DEMO_SETTING_VALUES = "Y" +``` + +If environment variables are not set, the demos will prompt you to enter values interactively. + +## Samples + +The migration samples are organized into three categories, each demonstrating different AI service integrations: + +|Category|Description| +|---|---| +|[AzureAIFoundry](./AzureAIFoundry/)|Azure OpenAI service integration samples| +|[OpenAI](./OpenAI/)|Direct OpenAI API integration samples| +|[OpenAIAssistants](./OpenAIAssistants/)|OpenAI Assistant API integration samples| + +## Running the samples from the console + +To run any migration sample, navigate to the desired sample directory: + +```powershell +# Azure AI Foundry Examples +cd "AzureAIFoundry\Step01_Basics" +dotnet run + +cd "AzureAIFoundry\Step03_ToolCall" +dotnet run + +# OpenAI Examples +cd "OpenAI\Step01_Basics" +dotnet run + +cd "OpenAI\Step02_DependencyInjection" +dotnet run + +# OpenAI Assistants Examples +cd "OpenAIAssistants\Step01_Basics" +dotnet run + +cd "OpenAIAssistants\Step04_CodeInterpreter" +dotnet run +``` \ No newline at end of file diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj index a9e91d1e7e..59ccb71e32 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj @@ -11,6 +11,7 @@ + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/NewPersistentAgentsChatClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/NewPersistentAgentsChatClient.cs index af5f1ca273..8748b581ab 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/NewPersistentAgentsChatClient.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/NewPersistentAgentsChatClient.cs @@ -9,6 +9,7 @@ #nullable enable +using System.Collections; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -19,7 +20,7 @@ using Microsoft.Extensions.AI; namespace Azure.AI.Agents.Persistent { /// Represents an for an Azure.AI.Agents.Persistent . - public partial class NewPersistentAgentsChatClient : IChatClient + internal partial class NewPersistentAgentsChatClient : IChatClient { /// The name of the chat client provider. private const string ProviderName = "azure"; @@ -42,14 +43,8 @@ namespace Azure.AI.Agents.Persistent /// Initializes a new instance of the class for the specified . public NewPersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { - if (client is null) - { - throw new ArgumentNullException(nameof(client)); - } - if (string.IsNullOrWhiteSpace(agentId)) - { - throw new ArgumentException("Value cannot be empty or contain only white-space characters.", nameof(agentId)); - } + Argument.AssertNotNull(client, nameof(client)); + Argument.AssertNotNullOrWhiteSpace(agentId, nameof(agentId)); _client = client; _agentId = agentId; @@ -58,11 +53,6 @@ namespace Azure.AI.Agents.Persistent _metadata = new(ProviderName); } - /// - /// Initializes a new instance of the class. - /// - public NewPersistentAgentsChatClient() { } - /// public virtual object? GetService(Type serviceType, object? serviceKey = null) => serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : @@ -73,18 +63,25 @@ namespace Azure.AI.Agents.Persistent null; /// - public virtual Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => - GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken); + public virtual async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + // Changing the original implementation to provide a RawRepresentation as a list of RawRepresentations of the updates. + // This wouldn't be needed if the API Change Proposal below is accepted: + // https://github.com/dotnet/extensions/issues/6746 + var updates = await GetStreamingResponseAsync(messages, options, cancellationToken).ToListAsync(cancellationToken).ConfigureAwait(false); + var response = updates.ToChatResponse(); + + // Expose all the raw representations of the updates. + response.RawRepresentation = updates.Select(u => u.RawRepresentation).ToArray(); + return response; + } /// public virtual async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - if (messages is null) - { - throw new ArgumentNullException(nameof(messages)); - } + Argument.AssertNotNull(messages, nameof(messages)); // Extract necessary state from messages and options. (ThreadAndRunOptions runOptions, List? toolResults) = @@ -113,7 +110,8 @@ namespace Azure.AI.Agents.Persistent // Submit the request. IAsyncEnumerable updates; - if (threadRun is not null && + if (toolResults is not null && + threadRun is not null && ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId && toolRunId == threadRun.Id) { @@ -139,24 +137,32 @@ namespace Azure.AI.Agents.Persistent } // Now create a new run and stream the results. + CreateRunStreamingOptions opts = new() + { + OverrideModelName = runOptions.OverrideModelName, + OverrideInstructions = runOptions.OverrideInstructions, + AdditionalInstructions = null, + AdditionalMessages = runOptions.ThreadOptions.Messages, + OverrideTools = runOptions.OverrideTools, + ToolResources = runOptions.ToolResources, + Temperature = runOptions.Temperature, + TopP = runOptions.TopP, + MaxPromptTokens = runOptions.MaxPromptTokens, + MaxCompletionTokens = runOptions.MaxCompletionTokens, + TruncationStrategy = runOptions.TruncationStrategy, + ToolChoice = runOptions.ToolChoice, + ResponseFormat = runOptions.ResponseFormat, + ParallelToolCalls = runOptions.ParallelToolCalls, + Metadata = runOptions.Metadata + }; + + // This method added for compatibility, before the include parameter support was enabled. updates = _client!.Runs.CreateRunStreamingAsync( threadId: threadId, agentId: _agentId, - overrideModelName: runOptions.OverrideModelName, - overrideInstructions: runOptions.OverrideInstructions, - additionalInstructions: null, - additionalMessages: runOptions.ThreadOptions.Messages, - overrideTools: runOptions.OverrideTools, - temperature: runOptions.Temperature, - topP: runOptions.TopP, - maxPromptTokens: runOptions.MaxPromptTokens, - maxCompletionTokens: runOptions.MaxCompletionTokens, - truncationStrategy: runOptions.TruncationStrategy, - toolChoice: runOptions.ToolChoice, - responseFormat: runOptions.ResponseFormat, - parallelToolCalls: runOptions.ParallelToolCalls, - metadata: runOptions.Metadata, - cancellationToken); + options: opts, + cancellationToken: cancellationToken + ); } // Process each update. @@ -208,18 +214,58 @@ namespace Azure.AI.Agents.Persistent break; case MessageContentUpdate mcu: - yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) + ChatResponseUpdate textUpdate = new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) { + AuthorName = _agentId, ConversationId = threadId, MessageId = responseId, RawRepresentation = mcu, ResponseId = responseId, }; + + // Add any annotations from the text update. The OpenAI Assistants API does not support passing these back + // into the model (MessageContent.FromXx does not support providing annotations), so they end up being one way and are dropped + // on subsequent requests. + if (mcu.TextAnnotation is { } tau) + { + string? fileId = null; + string? toolName = null; + if (!string.IsNullOrWhiteSpace(tau.InputFileId)) + { + fileId = tau.InputFileId; + toolName = "file_search"; + } + else if (!string.IsNullOrWhiteSpace(tau.OutputFileId)) + { + fileId = tau.OutputFileId; + toolName = "code_interpreter"; + } + + if (fileId is not null) + { + if (textUpdate.Contents.Count == 0) + { + // In case a chunk doesn't have text content, create one with empty text to hold the annotation. + textUpdate.Contents.Add(new TextContent(string.Empty)); + } + + (((TextContent)textUpdate.Contents[0]).Annotations ??= []).Add(new CitationAnnotation + { + RawRepresentation = tau, + AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = tau.StartIndex, EndIndex = tau.EndIndex }], + FileId = fileId, + ToolName = toolName, + }); + } + } + + yield return textUpdate; break; default: yield return new ChatResponseUpdate { + AuthorName = _agentId, ConversationId = threadId, MessageId = responseId, RawRepresentation = update, @@ -315,7 +361,10 @@ namespace Azure.AI.Agents.Persistent break; case HostedFileSearchTool fileSearchTool: - toolDefinitions.Add(new FileSearchToolDefinition()); + toolDefinitions.Add(new FileSearchToolDefinition() + { + FileSearch = new() { MaxNumResults = fileSearchTool.MaximumResultCount } + }); if (fileSearchTool.Inputs is { Count: > 0 }) { @@ -324,13 +373,11 @@ namespace Azure.AI.Agents.Persistent switch (input) { case HostedVectorStoreContent hostedVectorStore: - // If the input is a HostedFileContent, we can use its ID directly. (toolResources ??= new() { FileSearch = new() }).FileSearch.VectorStoreIds.Add(hostedVectorStore.VectorStoreId); break; } } } - break; case HostedWebSearchTool webSearch when webSearch.AdditionalProperties?.TryGetValue("connectionId", out object? connectionId) is true: @@ -356,7 +403,7 @@ namespace Azure.AI.Agents.Persistent switch (options.ToolMode) { case NoneChatToolMode: - runOptions.ToolChoice = BinaryData.FromString("none"); + runOptions.ToolChoice = BinaryData.FromString("\"none\""); break; case RequiredChatToolMode required: @@ -364,6 +411,9 @@ namespace Azure.AI.Agents.Persistent BinaryData.FromString($$"""{"type": "function", "function": {"name": "{{functionName}}"} }""") : BinaryData.FromString("required"); break; + case AutoChatToolMode: + runOptions.ToolChoice = BinaryData.FromString("\"auto\""); + break; } } @@ -402,6 +452,10 @@ namespace Azure.AI.Agents.Persistent runOptions.ResponseFormat = BinaryData.FromString("""{ "type": "json_object" }"""); } } + else if (options.ResponseFormat is ChatResponseFormatText textFormat) + { + runOptions.ResponseFormat = BinaryData.FromString("""{ "type": "text" }"""); + } } } @@ -510,7 +564,6 @@ namespace Azure.AI.Agents.Persistent // We need to extract the run ID and ensure that the ToolOutput we send back to Azure // is only the call ID. string[]? runAndCallIDs; -#pragma warning disable CA1031 // Do not catch general exception types try { runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, AgentsChatClientJsonContext.Default.StringArray); @@ -519,7 +572,6 @@ namespace Azure.AI.Agents.Persistent { continue; } -#pragma warning restore CA1031 // Do not catch general exception types if (runAndCallIDs is null || runAndCallIDs.Length != 2 || @@ -545,4 +597,120 @@ namespace Azure.AI.Agents.Persistent [JsonSerializable(typeof(IDictionary))] private sealed partial class AgentsChatClientJsonContext : JsonSerializerContext; } + + internal static class Argument + { + public static void AssertNotNull(T value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + + public static void AssertNotNull(T? value, string name) + where T : struct + { + if (!value.HasValue) + { + throw new ArgumentNullException(name); + } + } + + public static void AssertNotNullOrEmpty(IEnumerable value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + if (value is ICollection collectionOfT && collectionOfT.Count == 0) + { + throw new ArgumentException("Value cannot be an empty collection.", name); + } + if (value is ICollection collection && collection.Count == 0) + { + throw new ArgumentException("Value cannot be an empty collection.", name); + } + using IEnumerator e = value.GetEnumerator(); + if (!e.MoveNext()) + { + throw new ArgumentException("Value cannot be an empty collection.", name); + } + } + + public static void AssertNotNullOrEmpty(string value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + if (value.Length == 0) + { + throw new ArgumentException("Value cannot be an empty string.", name); + } + } + + public static void AssertNotNullOrWhiteSpace(string value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Value cannot be empty or contain only white-space characters.", name); + } + } + + public static void AssertNotDefault(ref T value, string name) + where T : struct, IEquatable + { + if (value.Equals(default)) + { + throw new ArgumentException("Value cannot be empty.", name); + } + } + + public static void AssertInRange(T value, T minimum, T maximum, string name) + where T : notnull, IComparable + { + if (minimum.CompareTo(value) > 0) + { + throw new ArgumentOutOfRangeException(name, "Value is less than the minimum allowed."); + } + if (maximum.CompareTo(value) < 0) + { + throw new ArgumentOutOfRangeException(name, "Value is greater than the maximum allowed."); + } + } + + public static void AssertEnumDefined(Type enumType, object value, string name) + { + if (!Enum.IsDefined(enumType, value)) + { + throw new ArgumentException($"Value not defined for {enumType.FullName}.", name); + } + } + + public static T CheckNotNull(T value, string name) + where T : class + { + AssertNotNull(value, name); + return value; + } + + public static string CheckNotNullOrEmpty(string value, string name) + { + AssertNotNullOrEmpty(value, name); + return value; + } + + public static void AssertNull(T value, string name, string? message = null) + { + if (value != null) + { + throw new ArgumentException(message ?? "Value must be null.", name); + } + } + } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs index d63cfb915e..d132387b5e 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs @@ -47,7 +47,7 @@ internal static class PersistentAgentResponseExtensions } #pragma warning disable CA2000 // Dispose objects before losing scope - var chatClient = new NewPersistentAgentsChatClient(persistentAgentsClient, persistentAgentMetadata.Id); + var chatClient = persistentAgentsClient.AsNewIChatClient(persistentAgentMetadata.Id); #pragma warning restore CA2000 // Dispose objects before losing scope return new ChatClientAgent(chatClient, options: new() diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs index acfa0a11ef..3df6caf488 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs @@ -10,6 +10,35 @@ namespace Azure.AI.Agents.Persistent; /// public static class PersistentAgentsClientExtensions { + /// + /// Retrieves an existing server side agent, wrapped as a using the provided . + /// + /// The to create the with. + /// A for the persistent agent. + /// The ID of the server side agent to create a for. + /// Options that should apply to all runs of the agent. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations on the persistent agent. + public static ChatClientAgent GetAIAgent( + this PersistentAgentsClient persistentAgentsClient, + string agentId, + ChatOptions? chatOptions = null, + CancellationToken cancellationToken = default) + { + if (persistentAgentsClient is null) + { + throw new ArgumentNullException(nameof(persistentAgentsClient)); + } + + if (string.IsNullOrWhiteSpace(agentId)) + { + throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId)); + } + + var persistentAgentResponse = persistentAgentsClient.Administration.GetAgent(agentId, cancellationToken); + return persistentAgentResponse.AsAIAgent(persistentAgentsClient, chatOptions); + } + /// /// Retrieves an existing server side agent, wrapped as a using the provided . /// @@ -89,4 +118,65 @@ public static class PersistentAgentsClientExtensions // Get a local proxy for the agent to work with. return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, cancellationToken: cancellationToken).ConfigureAwait(false); } + + /// + /// Creates a new server side agent using the provided . + /// + /// The to create the agent with. + /// The model to be used by the agent. + /// The name of the agent. + /// The description of the agent. + /// The instructions for the agent. + /// The tools to be used by the agent. + /// The resources for the tools. + /// The temperature setting for the agent. + /// The top-p setting for the agent. + /// The response format for the agent. + /// The metadata for the agent. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations on the newly created agent. + public static ChatClientAgent CreateAIAgent( + this PersistentAgentsClient persistentAgentsClient, + string model, + string? name = null, + string? description = null, + string? instructions = null, + IEnumerable? tools = null, + ToolResources? toolResources = null, + float? temperature = null, + float? topP = null, + BinaryData? responseFormat = null, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default) + { + if (persistentAgentsClient is null) + { + throw new ArgumentNullException(nameof(persistentAgentsClient)); + } + + var createPersistentAgentResponse = persistentAgentsClient.Administration.CreateAgent( + model, + name, + instructions, + tools: tools, + toolResources: toolResources, + temperature: temperature, + topP: topP, + responseFormat: responseFormat, + metadata: metadata, + cancellationToken: cancellationToken); + + // Get a local proxy for the agent to work with. + return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, cancellationToken: cancellationToken); + } + + /// + /// Creates a new instance of an configured for the specified assistant. + /// + /// The instance used to initialize the chat client. Cannot be . + /// The unique identifier of the assistant. Cannot be or whitespace. + /// The optional default thread identifier for the chat client. Can be . + /// A new instance configured with the specified assistant and optional default thread. + public static IChatClient AsNewIChatClient(this PersistentAgentsClient client, string assistantId, string? defaultThreadId = null) + => new NewPersistentAgentsChatClient(Argument.CheckNotNull(client, nameof(client)), Argument.CheckNotNullOrEmpty(assistantId, nameof(assistantId)), defaultThreadId); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj index eb4ad0143d..9446c6dd60 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj @@ -13,6 +13,7 @@ + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs index cd3408df8f..d02646d4d0 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs @@ -28,11 +28,10 @@ using OpenAI.Assistants; namespace Microsoft.Extensions.AI; -/// Represents an for an Azure.AI.Agents.Persistent . -public sealed class NewOpenAIAssistantChatClient : IChatClient +/// Represents an for an OpenAI . +internal sealed class NewOpenAIAssistantsChatClient : IChatClient { /// The underlying . -#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. private readonly AssistantClient _client; /// Metadata for the client. @@ -47,8 +46,8 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient /// List of tools associated with the assistant. private IReadOnlyList? _assistantTools; - /// Initializes a new instance of the class for the specified . - public NewOpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId = null) + /// Initializes a new instance of the class for the specified . + public NewOpenAIAssistantsChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId) { _client = Throw.IfNull(assistantClient); _assistantId = Throw.IfNullOrWhitespace(assistantId); @@ -65,6 +64,13 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient _metadata = new("openai", providerUrl); } + /// Initializes a new instance of the class for the specified . + public NewOpenAIAssistantsChatClient(AssistantClient assistantClient, Assistant assistant, string? defaultThreadId) + : this(assistantClient, Throw.IfNull(assistant).Id, defaultThreadId) + { + _assistantTools = assistant.Tools; + } + /// public object? GetService(Type serviceType, object? serviceKey = null) => serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : @@ -75,76 +81,18 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient null; /// - public Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => - GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken); - - private ToolResources? CreateToolResources(ChatOptions? options) + public async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { - if (options is null) - { - return null; - } + // Changing the original implementation to provide a RawRepresentation as a list of RawRepresentations of the updates. + // This wouldn't be needed if the API Change Proposal below is accepted: + // https://github.com/dotnet/extensions/issues/6746 + var updates = await GetStreamingResponseAsync(messages, options, cancellationToken).ToListAsync(cancellationToken).ConfigureAwait(false); + var response = updates.ToChatResponse(); - if (options.Tools is { Count: > 0 } tools) - { - FileSearchToolResources? fileSearchResources = null; - CodeInterpreterToolResources? codeInterpreterResources = null; - // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. - foreach (AITool tool in tools) - { - switch (tool) - { - case HostedCodeInterpreterTool codeTool: - - if (codeTool.Inputs is { Count: > 0 }) - { - codeInterpreterResources ??= new(); - foreach (var input in codeTool.Inputs) - { - switch (input) - { - case HostedFileContent fileContent: - // Use the file ID from the HostedFileContent. - codeInterpreterResources.FileIds.Add(fileContent.FileId); - break; - } - } - } - - break; - - case HostedFileSearchTool fileSearchTool: - - // Handle file IDs for file search tool - if (fileSearchTool.Inputs is { Count: > 0 }) - { - fileSearchResources ??= new(); - - foreach (var input in fileSearchTool.Inputs) - { - switch (input) - { - case HostedVectorStoreContent vectorStoreContent: - // Use the vector store ID from the HostedVectorStoreContent. - fileSearchResources.VectorStoreIds.Add(vectorStoreContent.VectorStoreId); - break; - } - } - } - - break; - } - } - - return new ToolResources - { - CodeInterpreter = codeInterpreterResources, - FileSearch = fileSearchResources, - }; - } - - return null; + // Expose all the raw representations of the updates. + response.RawRepresentation = updates.Select(u => u.RawRepresentation).ToArray(); + return response; } /// @@ -310,7 +258,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient { if (textUpdate.Contents.Count == 0) { - // Create a empty chunk of text content to hold the annotation. + // In case a chunk doesn't have text content, create one with empty text to hold the annotation. textUpdate.Contents.Add(new TextContent(string.Empty)); } @@ -756,3 +704,19 @@ internal static class OpenAIClientExtensions2 return functionParameters; } } + +/// +/// Temporary extension methods to assist with creating proposed changed instances +/// +public static class OpenAIAssistantsExtensions +{ + /// + /// Creates a new instance of an configured for the specified assistant. + /// + /// The instance used to initialize the chat client. Cannot be . + /// The unique identifier of the assistant. Cannot be or whitespace. + /// The optional default thread identifier for the chat client. Can be . + /// A new instance configured with the specified assistant and optional default thread. + public static IChatClient AsNewIChatClient(this AssistantClient client, string assistantId, string? defaultThreadId = null) => + new NewOpenAIAssistantsChatClient(Throw.IfNull(client), Throw.IfNullOrWhitespace(assistantId), defaultThreadId); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs index 2c84d3a8e8..e048c78fae 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs @@ -86,9 +86,11 @@ public static class OpenAIAssistantClientExtensions { switch (tool) { - case AIFunction aiFunction: - assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); - break; + // Attempting to set the tools at the agent level throws + // https://github.com/dotnet/extensions/issues/6743 + //case AIFunction aiFunction: + // assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); + // break; case HostedCodeInterpreterTool: var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition(); @@ -178,9 +180,11 @@ public static class OpenAIAssistantClientExtensions { switch (tool) { - case AIFunction aiFunction: - assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); - break; + // Attempting to set the tools at the agent level throws + // https://github.com/dotnet/extensions/issues/6743 + //case AIFunction aiFunction: + // assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); + // break; case HostedCodeInterpreterTool: var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition(); @@ -206,7 +210,7 @@ public static class OpenAIAssistantClientExtensions }; #pragma warning disable CA2000 // Dispose objects before losing scope - var chatClient = client.AsIChatClient(assistantId); + var chatClient = client.AsNewIChatClient(assistantId); #pragma warning restore CA2000 // Dispose objects before losing scope return new ChatClientAgent(chatClient, agentOptions, loggerFactory); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs index e33b3a7f9f..472ed7730d 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs @@ -55,7 +55,7 @@ public sealed class ChatClientAgent : AIAgent /// The chat client to use for invoking the agent. /// Full set of options to configure the agent. /// Optional logger factory to use for logging. - public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) + public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options, ILoggerFactory? loggerFactory = null) { _ = Throw.IfNull(chatClient); @@ -68,7 +68,7 @@ public sealed class ChatClientAgent : AIAgent this._chatClientType = chatClient.GetType(); // If the user has not opted out of using our default decorators, we wrap the chat client. - this.ChatClient = options?.UseProvidedChatClientAsIs is true ? chatClient : chatClient.AsAgentInvokedChatClient(); + this.ChatClient = options?.UseProvidedChatClientAsIs is true ? chatClient : chatClient.AsAgentInvokedChatClient(options); this._logger = (loggerFactory ?? chatClient.GetService() ?? NullLoggerFactory.Instance).CreateLogger(); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs index 4577946889..8085faf6f4 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs @@ -8,11 +8,12 @@ namespace Microsoft.Extensions.AI.Agents; internal static class ChatClientExtensions { - internal static IChatClient AsAgentInvokedChatClient(this IChatClient chatClient) + internal static IChatClient AsAgentInvokedChatClient(this IChatClient chatClient, ChatClientAgentOptions? options) { var chatBuilder = chatClient.AsBuilder(); - if (chatClient is not AgentInvokedChatClient agentInvokedChatClient) + // AgentInvokingChatClient should be the outermost decorator + if (chatClient is not AgentInvokedChatClient agentInvokingChatClient) { chatBuilder.UseAgentInvocation(); } @@ -27,6 +28,14 @@ internal static class ChatClientExtensions }); } - return chatBuilder.Build(); + var agentChatClient = chatBuilder.Build(); + + if (options?.ChatOptions?.Tools is { Count: > 0 }) + { + // When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client + agentChatClient.GetService()!.AdditionalTools = options.ChatOptions.Tools; + } + + return agentChatClient; } }