diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..5254fc8aa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Auto-detect text files, ensure they use LF. +* text=auto eol=lf working-tree-encoding=UTF-8 + +# Bash scripts +*.sh text eol=lf +*.cmd text eol=crlf \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/AgentSample.cs b/dotnet/samples/GettingStarted/AgentSample.cs index 62100ce34..e73dd06a8 100644 --- a/dotnet/samples/GettingStarted/AgentSample.cs +++ b/dotnet/samples/GettingStarted/AgentSample.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.ClientModel; +using Azure.AI.Agents.Persistent; using Azure.AI.OpenAI; using Azure.Identity; +using Microsoft.Agents; using Microsoft.Extensions.AI; using Microsoft.Shared.Samples; using OpenAI; @@ -20,45 +22,115 @@ public enum ChatClientProviders OpenAI, AzureOpenAI, OpenAIResponses, - OpenAIResponses_InMemoryMessage, - OpenAIResponses_ConversationId + OpenAIResponses_InMemoryMessageThread, + OpenAIResponses_ConversationIdThread, + AzureAIAgentsPersistent } - protected IChatClient GetChatClient(ChatClientProviders provider) + protected Task GetChatClientAsync(ChatClientProviders provider, ChatClientAgentOptions options, CancellationToken cancellationToken = default) => provider switch { - ChatClientProviders.OpenAI => GetOpenAIChatClient(), - ChatClientProviders.AzureOpenAI => GetAzureOpenAIChatClient(), + ChatClientProviders.OpenAI => GetOpenAIChatClientAsync(), + ChatClientProviders.AzureOpenAI => GetAzureOpenAIChatClientAsync(), + ChatClientProviders.AzureAIAgentsPersistent => GetAzureAIAgentPersistentClientAsync(options, cancellationToken), ChatClientProviders.OpenAIResponses or - ChatClientProviders.OpenAIResponses_InMemoryMessage or - ChatClientProviders.OpenAIResponses_ConversationId - => GetOpenAIResponsesClient(), + ChatClientProviders.OpenAIResponses_InMemoryMessageThread or + ChatClientProviders.OpenAIResponses_ConversationIdThread + => GetOpenAIResponsesClientAsync(), _ => throw new NotSupportedException($"Provider {provider} is not supported.") }; protected ChatOptions? GetChatOptions(ChatClientProviders? provider) => provider switch { - ChatClientProviders.OpenAIResponses_InMemoryMessage => new() { RawRepresentationFactory = static (_) => new ResponseCreationOptions() { StoredOutputEnabled = false } }, - ChatClientProviders.OpenAIResponses_ConversationId => new() { RawRepresentationFactory = static (_) => new ResponseCreationOptions() { StoredOutputEnabled = true } }, + ChatClientProviders.OpenAIResponses_InMemoryMessageThread => new() { RawRepresentationFactory = static (_) => new ResponseCreationOptions() { StoredOutputEnabled = false } }, + ChatClientProviders.OpenAIResponses_ConversationIdThread => new() { RawRepresentationFactory = static (_) => new ResponseCreationOptions() { StoredOutputEnabled = true } }, _ => null }; - private IChatClient GetOpenAIChatClient() - => new OpenAIClient(TestConfiguration.OpenAI.ApiKey) - .GetChatClient(TestConfiguration.OpenAI.ChatModelId) - .AsIChatClient(); - - private IChatClient GetAzureOpenAIChatClient() - => ((TestConfiguration.AzureOpenAI.ApiKey is null) - // Use Azure CLI credentials if API key is not provided. - ? new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new AzureCliCredential()) - : new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new ApiKeyCredential(TestConfiguration.AzureOpenAI.ApiKey))) - .GetChatClient(TestConfiguration.AzureOpenAI.DeploymentName) - .AsIChatClient(); - - private IChatClient GetOpenAIResponsesClient() - => new OpenAIClient(TestConfiguration.OpenAI.ApiKey) - .GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId) - .AsIChatClient(); + /// + /// For providers that store the agent and the thread on the server side, this will clean and delete + /// any sample agent and thread that was created during this execution. + /// + /// The chat client provider type that determines the cleanup process. + /// The agent instance to be cleaned up. + /// Optional thread associated with the agent that may also need to be cleaned up. + /// Cancellation token to monitor for cancellation requests. The default is . + /// + /// Ideally for faster execution and potential cost savings, server-side agents should be reused. + /// + protected Task AgentCleanUpAsync(ChatClientProviders provider, ChatClientAgent agent, AgentThread? thread = null, CancellationToken cancellationToken = default) + { + return provider switch + { + ChatClientProviders.AzureAIAgentsPersistent => AzureAIAgentsPersistentAgentCleanUpAsync(agent, thread, cancellationToken), + + // For other remaining provider sample types, no cleanup is needed as they don't offer a server-side agent/thread clean-up API. + _ => Task.CompletedTask + }; + } + + #region Private GetChatClient + + private Task GetOpenAIChatClientAsync() + => Task.FromResult( + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) + .GetChatClient(TestConfiguration.OpenAI.ChatModelId) + .AsIChatClient()); + + private Task GetAzureOpenAIChatClientAsync() + => Task.FromResult( + ((TestConfiguration.AzureOpenAI.ApiKey is null) + // Use Azure CLI credentials if API key is not provided. + ? new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new AzureCliCredential()) + : new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new ApiKeyCredential(TestConfiguration.AzureOpenAI.ApiKey))) + .GetChatClient(TestConfiguration.AzureOpenAI.DeploymentName) + .AsIChatClient()); + + private Task GetOpenAIResponsesClientAsync() + => Task.FromResult( + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) + .GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId) + .AsIChatClient()); + + private async Task GetAzureAIAgentPersistentClientAsync(ChatClientAgentOptions options, CancellationToken cancellationToken) + { + // Get a client to create server side agents with. + var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()); + + // Create a server side agent to work with. + var persistentAgentResponse = await persistentAgentsClient.Administration.CreateAgentAsync( + model: TestConfiguration.AzureAI.DeploymentName, + name: options.Name, + instructions: options.Instructions, + cancellationToken: cancellationToken); + + var persistentAgent = persistentAgentResponse.Value; + + // Get the chat client to use for the agent. + return persistentAgentsClient.AsIChatClient(persistentAgent.Id); + } + + #endregion + + #region Private AgentCleanUp + + private async Task AzureAIAgentsPersistentAgentCleanUpAsync(ChatClientAgent agent, AgentThread? thread, CancellationToken cancellationToken) + { + var persistentAgentsClient = agent.ChatClient.GetService(); + if (persistentAgentsClient is null) + { + throw new InvalidOperationException("The provided chat client is not a Persistent Agents Chat Client"); + } + + await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id, cancellationToken); + + // If a thread is provided, delete it as well. + if (thread is not null) + { + await persistentAgentsClient.Threads.DeleteThreadAsync(thread.Id, cancellationToken); + } + } + + #endregion } diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj index d3ccec945..36f6bac7b 100644 --- a/dotnet/samples/GettingStarted/GettingStarted.csproj +++ b/dotnet/samples/GettingStarted/GettingStarted.csproj @@ -34,4 +34,4 @@ - + \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs index 556cca506..b95000607 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs +++ b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs @@ -4,7 +4,6 @@ using Azure.Identity; using Microsoft.Agents; using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.AzureAIAgentsPersistent; using Microsoft.Shared.Samples; namespace Providers; @@ -37,7 +36,7 @@ public async Task RunWithAzureAIAgentsPersistent() // Get the chat client to use for the agent. using var chatClient = persistentAgentsClient.AsIChatClient(persistentAgent.Id); - // Define the agent + // Define the agent. ChatClientAgent agent = new(chatClient); // Start a new thread for the agent conversation. diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs index 650f727d5..022fb567e 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs @@ -38,7 +38,7 @@ public async Task RunWithOpenAIAssistant() // Get the chat client to use for the agent. using var chatClient = assistantClient.AsIChatClient(assistantId); - // Define the agent + // Define the agent. ChatClientAgent agent = new(chatClient); // Start a new thread for the agent conversation. diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs index 84062d47f..2bb07e20c 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs +++ b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs @@ -34,7 +34,7 @@ public async Task RunWithChatCompletion() // Start a new thread for the agent conversation. AgentThread thread = agent.GetNewThread(); - // Respond to user input + // Respond to user input. await RunAgentAsync("Tell me a joke about a pirate."); await RunAgentAsync("Now add some emojis to the joke."); diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs index 9d7f32335..e0d84ebed 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs +++ b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs @@ -43,7 +43,7 @@ public async Task RunWithChatCompletion(bool useConversationIdThread) // Start a new thread for the agent conversation based on the type. AgentThread thread = agent.GetNewThread(); - // Respond to user input + // Respond to user input. await RunAgentAsync("Tell me a joke about a pirate."); await RunAgentAsync("Now add some emojis to the joke."); diff --git a/dotnet/samples/GettingStarted/Steps/Step01_Running.cs b/dotnet/samples/GettingStarted/Steps/Step01_ChatClientAgent_Running.cs similarity index 66% rename from dotnet/samples/GettingStarted/Steps/Step01_Running.cs rename to dotnet/samples/GettingStarted/Steps/Step01_ChatClientAgent_Running.cs index baf4c8761..bd51c2e80 100644 --- a/dotnet/samples/GettingStarted/Steps/Step01_Running.cs +++ b/dotnet/samples/GettingStarted/Steps/Step01_ChatClientAgent_Running.cs @@ -10,14 +10,11 @@ namespace Steps; /// This class contains examples of using to showcase scenarios with and without conversation history. /// Each test method demonstrates how to configure and interact with the agents, including handling user input and displaying responses. /// -public sealed class Step01_Running(ITestOutputHelper output) : AgentSample(output) +public sealed class Step01_ChatClientAgent_Running(ITestOutputHelper output) : AgentSample(output) { private const string ParrotName = "Parrot"; private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound."; - private const string JokerName = "Joker"; - private const string JokerInstructions = "You are good at telling jokes."; - /// /// Demonstrate the usage of where each invocation is /// a unique interaction with no conversation history between them. @@ -26,18 +23,21 @@ public sealed class Step01_Running(ITestOutputHelper output) : AgentSample(outpu [InlineData(ChatClientProviders.OpenAI)] [InlineData(ChatClientProviders.AzureOpenAI)] [InlineData(ChatClientProviders.OpenAIResponses)] + [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] public async Task RunWithoutThread(ChatClientProviders provider) { + // Define the options for the chat client agent. + var agentOptions = new ChatClientAgentOptions + { + Name = ParrotName, + Instructions = ParrotInstructions, + }; + // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider); + using var chatClient = await base.GetChatClientAsync(provider, agentOptions); // Define the agent - ChatClientAgent agent = - new(chatClient, new() - { - Name = ParrotName, - Instructions = ParrotInstructions, - }); + var agent = new ChatClientAgent(chatClient, agentOptions); // Respond to user input await RunAgentAsync("Fortune favors the bold."); @@ -52,6 +52,9 @@ async Task RunAgentAsync(string input) var response = await agent.RunAsync(input); this.WriteResponseOutput(response); } + + // Clean up the agent after use when applicable. + await base.AgentCleanUpAsync(provider, agent); } /// @@ -60,24 +63,26 @@ async Task RunAgentAsync(string input) [Theory] [InlineData(ChatClientProviders.OpenAI)] [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessage)] - [InlineData(ChatClientProviders.OpenAIResponses_ConversationId)] + [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] + [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)] + [InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)] public async Task RunWithThread(ChatClientProviders provider) { - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider); + // Define the options for the chat client agent. + var agentOptions = new ChatClientAgentOptions + { + Name = ParrotName, + Instructions = ParrotInstructions, - // Get chat options based on the store type, if needed. - var chatOptions = base.GetChatOptions(provider); + // Get chat options based on the store type, if needed. + ChatOptions = base.GetChatOptions(provider), + }; + + // Get the chat client to use for the agent. + using var chatClient = await base.GetChatClientAsync(provider, agentOptions); // Define the agent - ChatClientAgent agent = - new(chatClient, new() - { - Name = JokerName, - Instructions = JokerInstructions, - ChatOptions = chatOptions, - }); + var agent = new ChatClientAgent(chatClient, agentOptions); // Start a new thread for the agent conversation. AgentThread thread = agent.GetNewThread(); @@ -95,6 +100,9 @@ async Task RunAgentAsync(string input) this.WriteResponseOutput(response); } + + // Clean up the agent and thread after use when applicable. + await base.AgentCleanUpAsync(provider, agent, thread); } /// @@ -104,24 +112,26 @@ async Task RunAgentAsync(string input) [Theory] [InlineData(ChatClientProviders.OpenAI)] [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessage)] - [InlineData(ChatClientProviders.OpenAIResponses_ConversationId)] + [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] + [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)] + [InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)] public async Task RunStreamingWithThread(ChatClientProviders provider) { - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider); + // Define the options for the chat client agent. + var agentOptions = new ChatClientAgentOptions + { + Name = ParrotName, + Instructions = ParrotInstructions, - // Get chat options based on the store type, if needed. - var chatOptions = base.GetChatOptions(provider); + // Get chat options based on the store type, if needed. + ChatOptions = base.GetChatOptions(provider), + }; + + // Get the chat client to use for the agent. + using var chatClient = await base.GetChatClientAsync(provider, agentOptions); // Define the agent - ChatClientAgent agent = - new(chatClient, new() - { - Name = ParrotName, - Instructions = ParrotInstructions, - ChatOptions = chatOptions, - }); + var agent = new ChatClientAgent(chatClient, agentOptions); // Start a new thread for the agent conversation. AgentThread thread = agent.GetNewThread(); @@ -140,5 +150,8 @@ async Task RunAgentAsync(string input) this.WriteAgentOutput(update); } } + + // Clean up the agent and thread after use when applicable. + await base.AgentCleanUpAsync(provider, agent, thread); } } diff --git a/dotnet/samples/GettingStarted/Steps/Step02_UsingTools.cs b/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingTools.cs similarity index 68% rename from dotnet/samples/GettingStarted/Steps/Step02_UsingTools.cs rename to dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingTools.cs index a97299927..8ce98d112 100644 --- a/dotnet/samples/GettingStarted/Steps/Step02_UsingTools.cs +++ b/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingTools.cs @@ -6,32 +6,38 @@ namespace Steps; -public sealed class Step02_UsingTools(ITestOutputHelper output) : AgentSample(output) +public sealed class Step02_ChatClientAgent_UsingTools(ITestOutputHelper output) : AgentSample(output) { [Theory] [InlineData(ChatClientProviders.OpenAI)] [InlineData(ChatClientProviders.AzureOpenAI)] public async Task RunningWithTools(ChatClientProviders provider) { + // Creating a Menu Tools to be used by the agent. + var menuTools = new MenuTools(); + + // Define the options for the chat client agent. + var agentOptions = new ChatClientAgentOptions + { + Name = "Host", + Instructions = "Answer questions about the menu.", + + // Provide the tools that are available to the agent + ChatOptions = new() + { + Tools = [ + AIFunctionFactory.Create(menuTools.GetMenu), + AIFunctionFactory.Create(menuTools.GetSpecials), + AIFunctionFactory.Create(menuTools.GetItemPrice) + ] + }, + }; + // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider); + using var chatClient = await base.GetChatClientAsync(provider, agentOptions); // Define the agent - var menuTools = new MenuTools(); - ChatClientAgent agent = - new(chatClient, new() - { - Name = "Host", - Instructions = "Answer questions about the menu.", - ChatOptions = new() - { - Tools = [ - AIFunctionFactory.Create(menuTools.GetMenu), - AIFunctionFactory.Create(menuTools.GetSpecials), - AIFunctionFactory.Create(menuTools.GetItemPrice) - ] - } - }); + var agent = new ChatClientAgent(chatClient, agentOptions); // Create the chat history thread to capture the agent interaction. var thread = agent.GetNewThread(); @@ -55,25 +61,31 @@ async Task RunAgentAsync(string input) [InlineData(ChatClientProviders.AzureOpenAI)] public async Task StreamingRunWithTools(ChatClientProviders provider) { + // Creating a Menu Tools to be used by the agent. + var menuTools = new MenuTools(); + + // Define the options for the chat client agent. + var agentOptions = new ChatClientAgentOptions + { + Name = "Host", + Instructions = "Answer questions about the menu.", + + // Provide the tools that are available to the agent + ChatOptions = new() + { + Tools = [ + AIFunctionFactory.Create(menuTools.GetMenu), + AIFunctionFactory.Create(menuTools.GetSpecials), + AIFunctionFactory.Create(menuTools.GetItemPrice) + ] + }, + }; + // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider); + using var chatClient = await base.GetChatClientAsync(provider, agentOptions); // Define the agent - var menuTools = new MenuTools(); - ChatClientAgent agent = - new(chatClient, new() - { - Name = "Host", - Instructions = "Answer questions about the menu.", - ChatOptions = new() - { - Tools = [ - AIFunctionFactory.Create(menuTools.GetMenu), - AIFunctionFactory.Create(menuTools.GetSpecials), - AIFunctionFactory.Create(menuTools.GetItemPrice) - ] - } - }); + var agent = new ChatClientAgent(chatClient, agentOptions); // Create the chat history thread to capture the agent interaction. var thread = agent.GetNewThread(); diff --git a/dotnet/src/Microsoft.Extensions.AI.AzureAIAgentsPersistent/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.AzureAIAgentsPersistent/PersistentAgentsClientExtensions.cs index a70d3e91b..9d3402517 100644 --- a/dotnet/src/Microsoft.Extensions.AI.AzureAIAgentsPersistent/PersistentAgentsClientExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.AzureAIAgentsPersistent/PersistentAgentsClientExtensions.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using Azure.AI.Agents.Persistent; +using Microsoft.Extensions.AI.AzureAIAgentsPersistent; -namespace Microsoft.Extensions.AI.AzureAIAgentsPersistent; +namespace Microsoft.Extensions.AI; /// /// Provides extension methods for . diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs index b170519cb..3f9435ad4 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs @@ -10,7 +10,6 @@ using Azure.Identity; using Microsoft.Agents; using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.AzureAIAgentsPersistent; using Shared.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests;