diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 136c1a168d..16abdc9479 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -21,7 +21,14 @@ - + + + + + + + + @@ -128,9 +135,9 @@ - - + + @@ -152,9 +159,9 @@ - + diff --git a/dotnet/demos/MinimalConsole/Program.cs b/dotnet/demos/MinimalConsole/Program.cs index 0ebd6a3833..3daf37b8be 100644 --- a/dotnet/demos/MinimalConsole/Program.cs +++ b/dotnet/demos/MinimalConsole/Program.cs @@ -8,17 +8,17 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using OpenAI; -var azureOpenAIEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -var azureOpenAIDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; [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."; AIAgent agent = new AzureOpenAIClient( - new Uri(azureOpenAIEndpoint), + new Uri(endpoint), new AzureCliCredential()) - .GetChatClient(azureOpenAIDeploymentName) + .GetChatClient(deploymentName) .CreateAIAgent( instructions: "You are a helpful assistant, you can help the user with weather information.", tools: [AIFunctionFactory.Create(GetWeather)]); diff --git a/dotnet/samples/GettingStarted/Steps/Step01_ChatClientAgent_Running.cs b/dotnet/samples/GettingStarted/Steps/Step01_ChatClientAgent_Running.cs deleted file mode 100644 index b593c24f6a..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step01_ChatClientAgent_Running.cs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; - -namespace Steps; - -/// -/// Provides test methods to demonstrate the usage of chat agents with different interaction models. -/// -/// 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_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 most basic Agent case, where we do not have a server-side agent - /// but just an in-memory agent, backed by an inference service, - /// and we are invoking with text input, and getting back a text response. - /// - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task RunBasic(ChatClientProviders provider) - { - // Get the chat client to communicate with the inference service backing our agent. - // Any implementation of Microsoft.Extensions.AI.Agents.IChatClient can be used with the ChatClientAgent. - // See the Providers folder for examples on how to create chat clients for some sample providers. - IChatClient chatClient = base.GetChatClient(provider); - - // Define the agent - AIAgent agent = new ChatClientAgent(chatClient, ParrotInstructions); - - // Invoke the agent and output the text result. - Console.WriteLine(await agent.RunAsync("Fortune favors the bold.")); - } - - /// - /// Demonstrate the usage of where each invocation is - /// a unique interaction with no conversation history between them. - /// - [Theory] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task RunWithoutThread(ChatClientProviders provider) - { - // Define the options for the chat client agent. - var agentOptions = new ChatClientAgentOptions(name: ParrotName, instructions: ParrotInstructions); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Respond to user input - await RunAgentAsync("Fortune favors the bold."); - await RunAgentAsync("I came, I saw, I conquered."); - await RunAgentAsync("Practice makes perfect."); - - // Local function to invoke agent and display the conversation messages. - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - - var response = await agent.RunAsync(input); - this.WriteResponseOutput(response); - } - - // Clean up the server-side agent after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent); - } - - /// - /// Demonstrate the usage of where a conversation history is maintained. - /// - [Theory] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)] - [InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)] - public async Task RunWithThread(ChatClientProviders provider) - { - // Define the options for the chat client agent. - var agentOptions = new ChatClientAgentOptions - { - Name = JokerName, - Instructions = JokerInstructions, - - // Get chat options based on the store type, if needed. - ChatOptions = base.GetChatOptions(provider), - }; - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Start a new thread for the agent conversation. - AgentThread thread = agent.GetNewThread(); - - // Respond to user input - await RunAgentAsync("Tell me a joke about a pirate."); - await RunAgentAsync("Now add some emojis to the joke."); - - // Local function to invoke agent and display the conversation messages for the thread. - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - - var response = await agent.RunAsync(input, thread); - - this.WriteResponseOutput(response); - } - - // Clean up the server-side agent and thread after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } - - /// - /// Demonstrate the usage of in streaming mode, - /// where a conversation is maintained by the . - /// - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)] - [InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)] - public async Task RunStreamingWithThread(ChatClientProviders provider) - { - // Define the options for the chat client agent. - var agentOptions = new ChatClientAgentOptions(name: JokerName, instructions: JokerInstructions) - { - // Get chat options based on the store type, if needed. - ChatOptions = base.GetChatOptions(provider), - }; - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Start a new thread for the agent conversation. - AgentThread thread = agent.GetNewThread(); - - // Respond to user input - await RunAgentAsync("Tell me a joke about a pirate."); - await RunAgentAsync("Now add some emojis to the joke."); - - // Local function to invoke agent and display the conversation messages. - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - - await foreach (var update in agent.RunStreamingAsync(input, thread)) - { - this.WriteAgentOutput(update); - } - } - - // Clean up the server-side agent and thread after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } -} diff --git a/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingFunctionTools.cs b/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingFunctionTools.cs deleted file mode 100644 index be945edfbf..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingFunctionTools.cs +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; - -namespace Steps; - -/// -/// This sample demonstrates how to use a with function tools. -/// It includes examples of both streaming and non-streaming agent interactions. -/// -public sealed class Step02_ChatClientAgent_UsingFunctionTools(ITestOutputHelper output) : AgentSample(output) -{ - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task RunningWithTools(ChatClientProviders provider) - { - // Creating a MenuTools instance 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", - tools: [ - AIFunctionFactory.Create(menuTools.GetMenu), - AIFunctionFactory.Create(menuTools.GetSpecials), - AIFunctionFactory.Create(menuTools.GetItemPrice) - ]); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Create the chat history thread to capture the agent interaction. - var thread = agent.GetNewThread(); - - // Respond to user input, invoking functions where appropriate. - await RunAgentAsync("Hello"); - await RunAgentAsync("What is the special soup and its price?"); - await RunAgentAsync("What is the special drink and its price?"); - await RunAgentAsync("Thank you"); - - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - var response = await agent.RunAsync(input, thread); - this.WriteResponseOutput(response); - } - - // Clean up the server-side agent after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } - - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task StreamingRunWithTools(ChatClientProviders provider) - { - // Creating a MenuTools instance 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", - tools: [ - AIFunctionFactory.Create(menuTools.GetMenu), - AIFunctionFactory.Create(menuTools.GetSpecials), - AIFunctionFactory.Create(menuTools.GetItemPrice) - ]); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Create the chat history thread to capture the agent interaction. - var thread = agent.GetNewThread(); - - // Respond to user input, invoking functions where appropriate. - await RunAgentAsync("Hello"); - await RunAgentAsync("What is the special soup and its price?"); - await RunAgentAsync("What is the special drink and its price?"); - await RunAgentAsync("Thank you"); - - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - await foreach (var update in agent.RunStreamingAsync(input, thread)) - { - this.WriteAgentOutput(update); - } - } - - // Clean up the server-side agent after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } - - private sealed class MenuTools - { - [Description("Get the full menu items.")] - public MenuItem[] GetMenu() - { - return s_menuItems; - } - - [Description("Get the specials from the menu.")] - public IEnumerable GetSpecials() - { - return s_menuItems.Where(i => i.IsSpecial); - } - - [Description("Get the price of a menu item.")] - public float? GetItemPrice([Description("The name of the menu item.")] string menuItem) - { - return s_menuItems.FirstOrDefault(i => i.Name.Equals(menuItem, StringComparison.OrdinalIgnoreCase))?.Price; - } - - private static readonly MenuItem[] s_menuItems = [ - new() { Category = "Soup", Name = "Clam Chowder", Price = 4.95f, IsSpecial = true }, - new() { Category = "Soup", Name = "Tomato Soup", Price = 4.95f, IsSpecial = false }, - new() { Category = "Salad", Name = "Cobb Salad", Price = 9.99f }, - new() { Category = "Salad", Name = "House Salad", Price = 4.95f }, - new() { Category = "Drink", Name = "Chai Tea", Price = 2.95f, IsSpecial = true }, - new() { Category = "Drink", Name = "Soda", Price = 1.95f }, - ]; - - public sealed class MenuItem - { - public string Category { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public float Price { get; set; } - public bool IsSpecial { get; set; } - } - } -} diff --git a/dotnet/samples/GettingStarted/Steps/Step04_ChatClientAgent_DependencyInjection.cs b/dotnet/samples/GettingStarted/Steps/Step04_ChatClientAgent_DependencyInjection.cs deleted file mode 100644 index 7c233a8cc3..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step04_ChatClientAgent_DependencyInjection.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Steps; - -public sealed class Step04_ChatClientAgent_DependencyInjection(ITestOutputHelper output) : AgentSample(output) -{ - private const string JokerName = "Joker"; - private const string JokerInstructions = "You are good at telling jokes."; - - [Theory] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task RunningWithServiceCollection(ChatClientProviders provider) - { - // Adding multiple chat clients to the service collection. - var services = new ServiceCollection(); - - var agentOptions = new ChatClientAgentOptions(JokerInstructions, JokerName); - - services.AddLogging(); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - services.AddSingleton(agentOptions); - - services.AddChatClient((sp) => base.GetChatClient(provider, sp.GetRequiredService())); - - services.AddSingleton((sp) - => new ChatClientAgent( - chatClient: sp.GetRequiredService(), - options: sp.GetRequiredService(), - loggerFactory: sp.GetRequiredService())); - - // Build the service provider. - await using var serviceProvider = services.BuildServiceProvider(); - - // Get the agent from the service provider. - var agent = serviceProvider.GetRequiredService(); - - // Create the chat history thread to capture the agent interaction. - var thread = agent.GetNewThread(); - - Console.WriteLine($"Using chat client for provider: {provider}"); - - // Respond to user input, invoking functions where appropriate. - await RunAgentAsync("Tell me a joke about a pirate."); - await RunAgentAsync("Now add some emojis to the joke."); - - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - var response = await agent.RunAsync(input, thread); - this.WriteResponseOutput(response); - } - - // Clean up the agent and thread after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } -} diff --git a/dotnet/samples/GettingStarted/Steps/Step05_ChatClientAgent_Telemetry.cs b/dotnet/samples/GettingStarted/Steps/Step05_ChatClientAgent_Telemetry.cs deleted file mode 100644 index a929126192..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step05_ChatClientAgent_Telemetry.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.AI.Agents; -using OpenTelemetry; -using OpenTelemetry.Trace; - -namespace Steps; - -/// -/// Demonstrates how to use telemetry with using OpenTelemetry. -/// -public sealed class Step05_ChatClientAgent_Telemetry(ITestOutputHelper output) : AgentSample(output) -{ - /// - /// Demonstrates OpenTelemetry tracing with Agent Framework. - /// - [Theory] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task RunWithTelemetry(ChatClientProviders provider) - { - // Enable telemetry - AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true); - - // Create TracerProvider with console exporter - string sourceName = Guid.NewGuid().ToString(); - - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(sourceName) - .AddConsoleExporter() - .Build(); - - // Define agent options - var agentOptions = new ChatClientAgentOptions(name: "TelemetryAgent", instructions: "You are a helpful assistant."); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - using var chatClient = base.GetChatClient(provider, agentOptions); - var baseAgent = new ChatClientAgent(chatClient, agentOptions); - - // Wrap the agent with OpenTelemetry instrumentation - using var agent = baseAgent.WithOpenTelemetry(sourceName: sourceName); - var thread = agent.GetNewThread(); - - // Run agent interactions - await agent.RunAsync("What is artificial intelligence?", thread); - await agent.RunAsync("How does machine learning work?", thread); - - // Clean up - await base.AgentCleanUpAsync(provider, baseAgent, thread); - } -} diff --git a/dotnet/samples/GettingStarted/Steps/Step06_ChatClientAgent_StructuredOutputs.cs b/dotnet/samples/GettingStarted/Steps/Step06_ChatClientAgent_StructuredOutputs.cs deleted file mode 100644 index 059086aa2a..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step06_ChatClientAgent_StructuredOutputs.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; - -namespace Steps; - -/// -/// Demonstrates how to use structured outputs with . -/// -public sealed class Step06_ChatClientAgent_StructuredOutputs(ITestOutputHelper output) : AgentSample(output) -{ - /// - /// Demonstrates processing structured outputs using JSON schemas to extract information about a person. - /// - [Theory] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task RunWithCustomSchema(ChatClientProviders provider) - { - var agentOptions = new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant.") - { - ChatOptions = new() - { - ResponseFormat = ChatResponseFormatJson.ForJsonSchema( - schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)), - schemaName: "PersonInfo", - schemaDescription: "Information about a person including their name, age, and occupation" - ) - } - }; - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - using var chatClient = base.GetChatClient(provider, agentOptions); - - ChatClientAgent agent = new(chatClient, agentOptions); - - var thread = agent.GetNewThread(); - - const string Prompt = "Please provide information about John Smith, who is a 35-year-old software engineer."; - - var updates = agent.RunStreamingAsync(Prompt, thread); - var agentResponse = await updates.ToAgentRunResponseAsync(); - - var personInfo = agentResponse.Deserialize(JsonSerializerOptions.Web); - - Console.WriteLine("Assistant Output:"); - Console.WriteLine($"Name: {personInfo.Name}"); - Console.WriteLine($"Age: {personInfo.Age}"); - Console.WriteLine($"Occupation: {personInfo.Occupation}"); - - // Clean up the server-side agent after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } - - /// - /// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent. - /// - public class PersonInfo - { - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("age")] - public int? Age { get; set; } - - [JsonPropertyName("occupation")] - public string? Occupation { get; set; } - } -} diff --git a/dotnet/samples/GettingStarted/Steps/Step08_ChatClientAgent_SuspendResumeThread.cs b/dotnet/samples/GettingStarted/Steps/Step08_ChatClientAgent_SuspendResumeThread.cs deleted file mode 100644 index 515aae080a..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step08_ChatClientAgent_SuspendResumeThread.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Microsoft.Extensions.AI.Agents; - -namespace Steps; - -/// -/// Demonstrates how to suspend and resume a thread with the . -/// -public sealed class Step08_ChatClientAgent_SuspendResumeThread(ITestOutputHelper output) : AgentSample(output) -{ - private const string JokerName = "Joker"; - private const string JokerInstructions = "You are good at telling jokes."; - - /// - /// Demonstrate the usage of where a thread is suspended. - /// The thread is serialized and can be stored to a database, file, or any other storage mechanism, - /// and then deserialized later to resume the conversation with the agent. - /// - [Theory] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)] - [InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)] - public async Task SuspendResumeThread(ChatClientProviders provider) - { - // Define the options for the chat client agent. - var agentOptions = new ChatClientAgentOptions - { - Name = JokerName, - Instructions = JokerInstructions, - - // Get chat options based on the store type, if needed. - ChatOptions = base.GetChatOptions(provider), - }; - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Start a new thread for the agent conversation. - AgentThread thread = agent.GetNewThread(); - - // Respond to user input - Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread)); - - // Serialize the thread state, so it can be stored for later use. - JsonElement serializedThread = await thread.SerializeAsync(); - - // The thread can now be saved to a database, file, or any other storage mechanism - // and loaded again later. - - // Deserialize the thread state after loading from storage. - AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread); - - Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread)); - - // Clean up the server-side agent and thread after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } -} diff --git a/dotnet/samples/GettingStarted/Steps/Step10_ChatClientAgent_UsingFunctionToolsWithApprovals.cs b/dotnet/samples/GettingStarted/Steps/Step10_ChatClientAgent_UsingFunctionToolsWithApprovals.cs deleted file mode 100644 index 7ae7a8d631..0000000000 --- a/dotnet/samples/GettingStarted/Steps/Step10_ChatClientAgent_UsingFunctionToolsWithApprovals.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; - -namespace Steps; - -/// -/// Demonstrates how to indicate that certain function calls require user approval before they can be executed and how to then approve or reject those function calls. -/// -public sealed class Step10_ChatClientAgent_UsingFunctionToolsWithApprovals(ITestOutputHelper output) : AgentSample(output) -{ - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task ApprovalsWithTools(ChatClientProviders provider) - { - // Creating a MenuTools instance to be used by the agent. - var menuTools = new MenuTools(); - - // Define the options for the chat client agent. - // We mark GetMenu and GetSpecial as requiring approval before they can be invoked, while GetItemPrice can be invoked without user approval. - // IMPORTANT: A limitation of the approvals flow when using ChatClientAgent is that if more than one function needs to be executed in one run, - // and any one of them requires approval, approval will be sought for all function calls produced during that run. - var agentOptions = new ChatClientAgentOptions( - name: "Host", - instructions: "Answer questions about the menu", - tools: [ - new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetMenu)), - new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetSpecials)), - AIFunctionFactory.Create(menuTools.GetItemPrice) - ]); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Create the chat history thread to capture the agent interaction. - var thread = agent.GetNewThread(); - - // Respond to user input, invoking functions where appropriate. - await RunAgentAsync("What is the special soup and its price?"); - await RunAgentAsync("What is the special drink?"); - - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - var response = await agent.RunAsync(input, thread); - - // Loop until all user input requests are handled. - var userInputRequests = response.UserInputRequests.ToList(); - while (userInputRequests.Count > 0) - { - // Approve GetSpecials function calls, reject all others. - List nextIterationMessages = userInputRequests?.Select((request) => request switch - { - FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" => - new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]), - - FunctionApprovalRequestContent functionApprovalRequest => - new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: false)]), - - _ => throw new NotSupportedException($"Unsupported user input request type: {request.GetType().Name}") - })?.ToList() ?? []; - - // Write out what the decision was for each function approval request. - nextIterationMessages.ForEach(x => Console.WriteLine($"Approval for the {(x.Contents[0] as FunctionApprovalResponseContent)?.FunctionCall.Name} function call is set to {(x.Contents[0] as FunctionApprovalResponseContent)?.Approved}.")); - - // Pass the user input responses back to the agent for further processing. - response = await agent.RunAsync(nextIterationMessages, thread); - - userInputRequests = response.UserInputRequests.ToList(); - } - - this.WriteResponseOutput(response); - } - - // Clean up the server-side agent after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } - - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.AzureAIAgentsPersistent)] - [InlineData(ChatClientProviders.OpenAIAssistant)] - [InlineData(ChatClientProviders.OpenAIChatCompletion)] - [InlineData(ChatClientProviders.OpenAIResponses)] - public async Task ApprovalsWithToolsStreaming(ChatClientProviders provider) - { - // Creating a MenuTools instance to be used by the agent. - var menuTools = new MenuTools(); - - // Creating a MenuTools instance to be used by the agent. - // We mark GetMenu and GetSpecial as requiring approval before they can be invoked, while GetItemPrice can be invoked without user approval. - // IMPORTANT: A limitation of the approvals flow when using ChatClientAgent is that if more than one function needs to be executed in one run, - // and any one of them requires approval, approval will be sought for all function calls produced during that run. - var agentOptions = new ChatClientAgentOptions( - name: "Host", - instructions: "Answer questions about the menu", - tools: [ - new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetMenu)), - new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetSpecials)), - AIFunctionFactory.Create(menuTools.GetItemPrice), - ]); - - // Create the server-side agent Id when applicable (depending on the provider). - agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions); - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Create the chat history thread to capture the agent interaction. - var thread = agent.GetNewThread(); - - // Respond to user input, invoking functions where appropriate. - await RunAgentAsync("What is the special soup and its price?"); - await RunAgentAsync("What is the special drink?"); - - async Task RunAgentAsync(string input) - { - this.WriteUserMessage(input); - var updates = await agent.RunStreamingAsync(input, thread).ToListAsync(); - - // Loop until all user input requests are handled. - var userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList(); - while (userInputRequests.Count > 0) - { - // Approve GetSpecials function calls, reject all others. - List nextIterationMessages = userInputRequests?.Select((request) => request switch - { - FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" => - new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]), - - FunctionApprovalRequestContent functionApprovalRequest => - new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: false)]), - - _ => throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}") - })?.ToList() ?? []; - - // Write out what the decision was for each function approval request. - nextIterationMessages.ForEach(x => Console.WriteLine($"Approval for the {(x.Contents[0] as FunctionApprovalResponseContent)?.FunctionCall.Name} function call is set to {(x.Contents[0] as FunctionApprovalResponseContent)?.Approved}.")); - - // Pass the user input responses back to the agent for further processing. - updates = await agent.RunStreamingAsync(nextIterationMessages, thread).ToListAsync(); - - userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList(); - } - - this.WriteResponseOutput(updates.ToAgentRunResponse()); - } - - // Clean up the server-side agent after use when applicable (depending on the provider). - await base.AgentCleanUpAsync(provider, agent, thread); - } - - private sealed class MenuTools - { - [Description("Get the full menu items.")] - public MenuItem[] GetMenu() - { - return s_menuItems; - } - - [Description("Get the specials from the menu.")] - public IEnumerable GetSpecials() - { - return s_menuItems.Where(i => i.IsSpecial); - } - - [Description("Get the price of a menu item.")] - public float? GetItemPrice([Description("The name of the menu item.")] string menuItem) - { - return s_menuItems.FirstOrDefault(i => i.Name.Equals(menuItem, StringComparison.OrdinalIgnoreCase))?.Price; - } - - private static readonly MenuItem[] s_menuItems = [ - new() { Category = "Soup", Name = "Clam Chowder", Price = 4.95f, IsSpecial = true }, - new() { Category = "Soup", Name = "Tomato Soup", Price = 4.95f, IsSpecial = false }, - new() { Category = "Salad", Name = "Cobb Salad", Price = 9.99f }, - new() { Category = "Salad", Name = "House Salad", Price = 4.95f }, - new() { Category = "Drink", Name = "Chai Tea", Price = 2.95f, IsSpecial = true }, - new() { Category = "Drink", Name = "Soda", Price = 1.95f }, - ]; - - public sealed class MenuItem - { - public string Category { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public float Price { get; set; } - public bool IsSpecial { get; set; } - } - } -} diff --git a/dotnet/samples/GettingStartedSteps/README.md b/dotnet/samples/GettingStartedSteps/README.md index 4b204637f4..cdce13ca5a 100644 --- a/dotnet/samples/GettingStartedSteps/README.md +++ b/dotnet/samples/GettingStartedSteps/README.md @@ -24,7 +24,14 @@ Before you begin, ensure you have the following prerequisites: |Sample|Description| |---|---| |[Running a simple agent](./Step01_ChatClientAgent_Running/)|This sample demonstrates how to create and run a basic agent with instructions| -|[Multi-turn conversation with a simple agent](./Step02_ChatClientAgent_MultiTurn/)|This sample demonstrates how to implement a multi-turn conversation with a simple agent| +|[Multi-turn conversation with a simple agent](./Step02_ChatClientAgent_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a simple agent| +|[Using function tools with a simple agent](./Step03_ChatClientAgent_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent| +|[Using function tools with approvals](./Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution| +|[Structured output with a simple agent](./Step05_ChatClientAgent_StructuredOutput/)|This sample demonstrates how to use structured output with a simple agent| +|[Persisted conversations with a simple agent](./Step06_ChatClientAgent_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later. This is useful for cases where an agent is hosted in a stateless service| +|[3rd party thread storage with a simple agent](./Step07_ChatClientAgent_3rdPartyThreadStorage/)|This sample demonstrates how to store conversation history in a 3rd party storage solution| +|[Telemetry with a simple agent](./Step08_ChatClientAgent_Telemetry/)|This sample demonstrates how to add telemetry to a simple agent| +|[Dependency injection with a simple agent](./Step09_ChatClientAgent_DependencyInjection/)|This sample demonstrates how to add and resolve an agent with a dependency injection container| ## Running the samples from the console diff --git a/dotnet/samples/GettingStartedSteps/Step01_ChatClientAgent_Running/Program.cs b/dotnet/samples/GettingStartedSteps/Step01_ChatClientAgent_Running/Program.cs index c2af5c010e..942c0cd9b1 100644 --- a/dotnet/samples/GettingStartedSteps/Step01_ChatClientAgent_Running/Program.cs +++ b/dotnet/samples/GettingStartedSteps/Step01_ChatClientAgent_Running/Program.cs @@ -8,25 +8,23 @@ using Azure.Identity; using Microsoft.Extensions.AI.Agents; using OpenAI; -var azureOpenAIEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -var azureOpenAIDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; const string JokerName = "Joker"; const string JokerInstructions = "You are good at telling jokes."; AIAgent agent = new AzureOpenAIClient( - new Uri(azureOpenAIEndpoint), + new Uri(endpoint), new AzureCliCredential()) - .GetChatClient(azureOpenAIDeploymentName) + .GetChatClient(deploymentName) .CreateAIAgent(JokerInstructions, JokerName); // Invoke the agent and output the text result. -Console.WriteLine("--- Run the agent ---\n"); Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); // Invoke the agent with streaming support. -Console.WriteLine("\n--- Run the agent with streaming ---\n"); await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.")) { - Console.Write(update); + Console.WriteLine(update); } diff --git a/dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_Multiturn/Program.cs b/dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_MultiturnConversation/Program.cs similarity index 71% rename from dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_Multiturn/Program.cs rename to dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_MultiturnConversation/Program.cs index 6b2d0d902a..e50dcbe9ca 100644 --- a/dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_Multiturn/Program.cs +++ b/dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_MultiturnConversation/Program.cs @@ -8,26 +8,24 @@ using Azure.Identity; using Microsoft.Extensions.AI.Agents; using OpenAI; -var azureOpenAIEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -var azureOpenAIDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; const string JokerName = "Joker"; const string JokerInstructions = "You are good at telling jokes."; AIAgent agent = new AzureOpenAIClient( - new Uri(azureOpenAIEndpoint), + new Uri(endpoint), new AzureCliCredential()) - .GetChatClient(azureOpenAIDeploymentName) + .GetChatClient(deploymentName) .CreateAIAgent(JokerInstructions, JokerName); // Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object. -Console.WriteLine("\n--- Run with a thread (context preserved) ---\n"); AgentThread thread = agent.GetNewThread(); Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread)); Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread)); // Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the thread object. -Console.WriteLine("\n--- Run with a thread and streaming (context preserved) ---\n"); thread = agent.GetNewThread(); await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.", thread)) { diff --git a/dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_Multiturn/Step02_ChatClientAgent_Multiturn.csproj b/dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_MultiturnConversation/Step02_ChatClientAgent_MultiturnConversation.csproj similarity index 100% rename from dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_Multiturn/Step02_ChatClientAgent_Multiturn.csproj rename to dotnet/samples/GettingStartedSteps/Step02_ChatClientAgent_MultiturnConversation/Step02_ChatClientAgent_MultiturnConversation.csproj diff --git a/dotnet/samples/GettingStartedSteps/Step03_ChatClientAgent_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStartedSteps/Step03_ChatClientAgent_UsingFunctionTools/Program.cs new file mode 100644 index 0000000000..18b05ba6ae --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step03_ChatClientAgent_UsingFunctionTools/Program.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use a ChatClientAgent with function tools. +// It shows both non-streaming and streaming agent interactions using menu-related tools. + +using System; +using System.ComponentModel; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using OpenAI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +[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."; + +// Create the chat client and agent, and provide the function tool to the agent. +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]); + +// Non-streaming agent interaction with function tools. +Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?")); + +// Streaming agent interaction with function tools. +await foreach (var update in agent.RunStreamingAsync("What is the weather like in Amsterdam?")) +{ + Console.WriteLine(update); +} diff --git a/dotnet/samples/GettingStartedSteps/Step03_ChatClientAgent_UsingFunctionTools/Step03_ChatClientAgent_UsingFunctionTools.csproj b/dotnet/samples/GettingStartedSteps/Step03_ChatClientAgent_UsingFunctionTools/Step03_ChatClientAgent_UsingFunctionTools.csproj new file mode 100644 index 0000000000..15185f869a --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step03_ChatClientAgent_UsingFunctionTools/Step03_ChatClientAgent_UsingFunctionTools.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStartedSteps/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/Program.cs b/dotnet/samples/GettingStartedSteps/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/Program.cs new file mode 100644 index 0000000000..8887f6cbf5 --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/Program.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use a ChatClientAgent with function tools that require a human in the loop for approvals. +// It shows both non-streaming and streaming agent interactions using menu-related tools. +// If the agent is hosted in a service, with a remote user, combine this sample with the Persisted Conversations sample to persist the chat history +// while the agent is waiting for user input. + +using System; +using System.ComponentModel; +using System.Linq; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using OpenAI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// Create a sample function tool that the agent can use. +[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."; + +// Create the chat client and agent. +// Note that we are wrapping the function tool with ApprovalRequiredAIFunction to require user approval before invoking it. +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]); + +// Call the agent and check if there are any user input requests to handle. +AgentThread thread = agent.GetNewThread(); +var response = await agent.RunAsync("What is the weather like in Amsterdam?", thread); +var userInputRequests = response.UserInputRequests.ToList(); + +// For streaming use: +// var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread).ToListAsync(); +// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList(); + +while (userInputRequests.Count > 0) +{ + // Ask the user to approve each function call request. + // For simplicity, we are assuming here that only function approval requests are being made. + var userInputResponses = userInputRequests + .OfType() + .Select(functionApprovalRequest => + { + Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}"); + return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]); + }) + .ToList(); + + // Pass the user input responses back to the agent for further processing. + response = await agent.RunAsync(userInputResponses, thread); + + userInputRequests = response.UserInputRequests.ToList(); + + // For streaming use: + // updates = await agent.RunStreamingAsync(userInputResponses, thread).ToListAsync(); + // userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList(); +} + +Console.WriteLine($"\nAgent: {response}"); + +// For streaming use: +// Console.WriteLine($"\nAgent: {updates.ToAgentRunResponse()}"); diff --git a/dotnet/samples/GettingStartedSteps/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals.csproj b/dotnet/samples/GettingStartedSteps/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals.csproj new file mode 100644 index 0000000000..47e49128ab --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals/Step04_ChatClientAgent_UsingFunctionToolsWithApprovals.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStartedSteps/Step05_ChatClientAgent_StructuredOutput/Program.cs b/dotnet/samples/GettingStartedSteps/Step05_ChatClientAgent_StructuredOutput/Program.cs new file mode 100644 index 0000000000..d8b20ba974 --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step05_ChatClientAgent_StructuredOutput/Program.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with Azure OpenAI as the backend, to produce structured output using JSON schema from a class. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using OpenAI; +using SampleApp; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// Create the agent options, specifying the response format to use a JSON schema based on the PersonInfo class. +ChatClientAgentOptions agentOptions = new(name: "HelpfulAssistant", instructions: "You are a helpful assistant.") +{ + ChatOptions = new() + { + ResponseFormat = ChatResponseFormatJson.ForJsonSchema( + schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)), + schemaName: "PersonInfo", + schemaDescription: "Information about a person including their name, age, and occupation") + } +}; + +// Create the agent using Azure OpenAI. +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(agentOptions); + +// Invoke the agent with some unstructured input, to extract the structured information from. +var response = await agent.RunAsync("Please provide information about John Smith, who is a 35-year-old software engineer."); + +// Deserialize the response into the PersonInfo class. +var personInfo = response.Deserialize(JsonSerializerOptions.Web); + +Console.WriteLine("Assistant Output:"); +Console.WriteLine($"Name: {personInfo.Name}"); +Console.WriteLine($"Age: {personInfo.Age}"); +Console.WriteLine($"Occupation: {personInfo.Occupation}"); + +// Invoke the agent with some unstructured input while streaming, to extract the structured information from. +var updates = agent.RunStreamingAsync("Please provide information about John Smith, who is a 35-year-old software engineer."); + +// Assemble all the parts of the streamed output, since we can only deserialize once we have the full json, +// then deserialize the response into the PersonInfo class. +personInfo = (await updates.ToAgentRunResponseAsync()).Deserialize(JsonSerializerOptions.Web); + +Console.WriteLine("Assistant Output:"); +Console.WriteLine($"Name: {personInfo.Name}"); +Console.WriteLine($"Age: {personInfo.Age}"); +Console.WriteLine($"Occupation: {personInfo.Occupation}"); + +namespace SampleApp +{ + /// + /// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent. + /// + public class PersonInfo + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + [JsonPropertyName("occupation")] + public string? Occupation { get; set; } + } +} diff --git a/dotnet/samples/GettingStartedSteps/Step05_ChatClientAgent_StructuredOutput/Step05_ChatClientAgent_StructuredOutput.csproj b/dotnet/samples/GettingStartedSteps/Step05_ChatClientAgent_StructuredOutput/Step05_ChatClientAgent_StructuredOutput.csproj new file mode 100644 index 0000000000..15185f869a --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step05_ChatClientAgent_StructuredOutput/Step05_ChatClientAgent_StructuredOutput.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStartedSteps/Step06_ChatClientAgent_PersistedConversations/Program.cs b/dotnet/samples/GettingStartedSteps/Step06_ChatClientAgent_PersistedConversations/Program.cs new file mode 100644 index 0000000000..60a9b6a973 --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step06_ChatClientAgent_PersistedConversations/Program.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with a conversation that can be persisted to disk. + +using System; +using System.IO; +using System.Text.Json; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI.Agents; +using OpenAI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +const string JokerName = "Joker"; +const string JokerInstructions = "You are good at telling jokes."; + +// Create the agent +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(JokerInstructions, JokerName); + +// Start a new thread for the agent conversation. +AgentThread thread = agent.GetNewThread(); + +// Run the agent with a new thread. +Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread)); + +// Serialize the thread state to a JsonElement, so it can be stored for later use. +JsonElement serializedThread = await thread.SerializeAsync(); + +// Save the serialized thread to a temporary file (for demonstration purposes). +string tempFilePath = Path.GetTempFileName(); +await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedThread)); + +// Load the serialized thread from the temporary file (for demonstration purposes). +JsonElement reloadedSerializedThread = JsonSerializer.Deserialize(await File.ReadAllTextAsync(tempFilePath)); + +// Deserialize the thread state after loading from storage. +AgentThread resumedThread = await agent.DeserializeThreadAsync(reloadedSerializedThread); + +// Run the agent again with the resumed thread. +Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread)); diff --git a/dotnet/samples/GettingStartedSteps/Step06_ChatClientAgent_PersistedConversations/Step06_ChatClientAgent_PersistedConversations.csproj b/dotnet/samples/GettingStartedSteps/Step06_ChatClientAgent_PersistedConversations/Step06_ChatClientAgent_PersistedConversations.csproj new file mode 100644 index 0000000000..15185f869a --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step06_ChatClientAgent_PersistedConversations/Step06_ChatClientAgent_PersistedConversations.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Steps/Step09_ChatClientAgent_3rdPartyThreadStorage.cs b/dotnet/samples/GettingStartedSteps/Step07_ChatClientAgent_3rdPartyThreadStorage/Program.cs similarity index 54% rename from dotnet/samples/GettingStarted/Steps/Step09_ChatClientAgent_3rdPartyThreadStorage.cs rename to dotnet/samples/GettingStartedSteps/Step07_ChatClientAgent_3rdPartyThreadStorage/Program.cs index d411f4fcb6..11cbd7670f 100644 --- a/dotnet/samples/GettingStarted/Steps/Step09_ChatClientAgent_3rdPartyThreadStorage.cs +++ b/dotnet/samples/GettingStartedSteps/Step07_ChatClientAgent_3rdPartyThreadStorage/Program.cs @@ -1,79 +1,82 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances + +// This sample shows how to create and use a simple AI agent with a conversation that can be persisted to disk. + +using System; +using System.Collections.Generic; +using System.Linq; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.VectorData; using Microsoft.SemanticKernel.Connectors.InMemory; +using OpenAI; +using SampleApp; -namespace Steps; +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; -/// -/// Demonstrates how to store the chat history of a thread in a 3rd party store when using . -/// -public sealed class Step09_ChatClientAgent_3rdPartyThreadStorage(ITestOutputHelper output) : AgentSample(output) +const string JokerName = "Joker"; +const string JokerInstructions = "You are good at telling jokes."; + +// Create a vector store to store the chat messages in. +// Replace this with a vector store implementation of your choice if you want to persist the chat history to disk. +VectorStore vectorStore = new InMemoryVectorStore(); + +// Create the agent +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(new ChatClientAgentOptions + { + Name = JokerName, + Instructions = JokerInstructions, + ChatMessageStoreFactory = () => + { + // Create a new chat message store for this agent that stores the messages in a vector store. + // Each thread must get its own copy of the VectorChatMessageStore, since the store + // also contains the id that the thread is stored under. + return new VectorChatMessageStore(vectorStore); + } + }); + +// Start a new thread for the agent conversation. +AgentThread thread = agent.GetNewThread(); + +// Run the agent with the thread that stores conversation history in the vector store. +Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread)); + +// Serialize the thread state, so it can be stored for later use. +// Since the chat history is stored in the vector store, the serialized thread +// only contains the guid that the messages are stored under in the vector store. +JsonElement serializedThread = await thread.SerializeAsync(); + +Console.WriteLine("\n--- Serialized thread ---\n"); +Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true })); + +// The serialized thread can now be saved to a database, file, or any other storage mechanism +// and loaded again later. + +// Deserialize the thread state after loading from storage. +AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread); + +// Run the agent with the thread that stores conversation history in the vector store a second time. +Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread)); + +namespace SampleApp { - private const string JokerName = "Joker"; - private const string JokerInstructions = "You are good at telling jokes."; - - /// - /// Demonstrate storage of the chat history of a thread in a 3rd party store when using . - /// - /// - /// Note that this is only supported for services that do not already store the chat history in their own service. - /// - [Theory] - [InlineData(ChatClientProviders.AzureOpenAI)] - [InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)] - public async Task ThirdPartyStorageThread(ChatClientProviders provider) - { - VectorStore vectorStore = new InMemoryVectorStore(); - - // Define the options for the chat client agent. - var agentOptions = new ChatClientAgentOptions - { - Name = JokerName, - Instructions = JokerInstructions, - ChatMessageStoreFactory = () => - { - // Create a new chat message store for this agent that stores the messages in a vector store. - // Each thread must get its own copy of the VectorChatMessageStore, since the store - // also contains the id that the thread is stored under. - return new VectorChatMessageStore(vectorStore); - } - }; - - // Get the chat client to use for the agent. - using var chatClient = base.GetChatClient(provider, agentOptions); - - // Define the agent - var agent = new ChatClientAgent(chatClient, agentOptions); - - // Start a new thread for the agent conversation. - AgentThread thread = agent.GetNewThread(); - - // Respond to user input - Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread)); - - // Serialize the thread state, so it can be stored for later use. - // Since the chat history is stored in the vector store, the serialized there - // only contains the guid that the messages are stored under in the vector store. - JsonElement serializedThread = await thread.SerializeAsync(); - - // The serialized thread can now be saved to a database, file, or any other storage mechanism - // and loaded again later. - - // Deserialize the thread state after loading from storage. - AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread); - - Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread)); - } - /// /// A sample implementation of that stores chat messages in a vector store. /// /// The vector store to store the messages in. - private sealed class VectorChatMessageStore(VectorStore vectorStore) : IChatMessageStore + internal sealed class VectorChatMessageStore(VectorStore vectorStore) : IChatMessageStore { private string? _threadId; diff --git a/dotnet/samples/GettingStartedSteps/Step07_ChatClientAgent_3rdPartyThreadStorage/Step07_ChatClientAgent_3rdPartyThreadStorage.csproj b/dotnet/samples/GettingStartedSteps/Step07_ChatClientAgent_3rdPartyThreadStorage/Step07_ChatClientAgent_3rdPartyThreadStorage.csproj new file mode 100644 index 0000000000..750a1c9ad7 --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step07_ChatClientAgent_3rdPartyThreadStorage/Step07_ChatClientAgent_3rdPartyThreadStorage.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStartedSteps/Step08_ChatClientAgent_Telemetry/Program.cs b/dotnet/samples/GettingStartedSteps/Step08_ChatClientAgent_Telemetry/Program.cs new file mode 100644 index 0000000000..e0a32594ef --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step08_ChatClientAgent_Telemetry/Program.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with Azure OpenAI as the backend that logs telemetry using OpenTelemetry. + +using System; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI.Agents; +using OpenAI; +using OpenTelemetry; +using OpenTelemetry.Trace; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +const string JokerName = "Joker"; +const string JokerInstructions = "You are good at telling jokes."; + +// Enable telemetry +AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true); + +// Create TracerProvider with console exporter +// This will output the telemetry data to the console. +string sourceName = Guid.NewGuid().ToString(); +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(sourceName) + .AddConsoleExporter() + .Build(); + +// Create the agent, and enable OpenTelemetry instrumentation. +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(JokerInstructions, JokerName) + .WithOpenTelemetry(sourceName: sourceName); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); + +// Invoke the agent with streaming support. +await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.")) +{ + Console.WriteLine(update); +} diff --git a/dotnet/samples/GettingStartedSteps/Step08_ChatClientAgent_Telemetry/Step08_ChatClientAgent_Telemetry.csproj b/dotnet/samples/GettingStartedSteps/Step08_ChatClientAgent_Telemetry/Step08_ChatClientAgent_Telemetry.csproj new file mode 100644 index 0000000000..836c10a00f --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step08_ChatClientAgent_Telemetry/Step08_ChatClientAgent_Telemetry.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStartedSteps/Step09_ChatClientAgent_DependencyInjection/Program.cs b/dotnet/samples/GettingStartedSteps/Step09_ChatClientAgent_DependencyInjection/Program.cs new file mode 100644 index 0000000000..36b7f1bedc --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step09_ChatClientAgent_DependencyInjection/Program.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable CA1812 + +// This sample shows how to use dependency injection to register an AIAgent and use it from a hosted service with a user input chat loop. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// Create a host builder that we will register services with and then run. +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + +// Add agent options to the service collection. +const string JokerName = "Joker"; +const string JokerInstructions = "You are good at telling jokes."; +builder.Services.AddSingleton(new ChatClientAgentOptions(JokerInstructions, JokerName)); + +// Add a chat client to the service collection. +builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient()); + +// Add the AI agent to the service collection. +builder.Services.AddSingleton((sp) => new ChatClientAgent( + chatClient: sp.GetRequiredKeyedService("AzureOpenAI"), + options: sp.GetRequiredService())); + +// Add a sample service that will use the agent to respond to user input. +builder.Services.AddHostedService(); + +// Create a cancellation token and source to pass to the sample service that can +// be used to signal shutdown of the application. +CancellationTokenSource appShutdownCancellationTokenSource = new(); +CancellationToken appShutdownCancellationToken = appShutdownCancellationTokenSource.Token; +builder.Services.AddKeyedSingleton("AppShutdown", appShutdownCancellationTokenSource); + +// Build and run the host. +using IHost host = builder.Build(); +await host.RunAsync(appShutdownCancellationToken).ConfigureAwait(false); + +namespace SampleApp +{ + /// + /// A sample service that uses an AI agent to respond to user input. + /// + internal sealed class SampleService(AIAgent agent, [FromKeyedServices("AppShutdown")] CancellationTokenSource appShutdownCancellationTokenSource) : IHostedService + { + private AgentThread? _thread; + + public async Task StartAsync(CancellationToken cancellationToken) + { + // Create a thread that will be used for the entirety of the service lifetime so that the user can ask follow up questions. + this._thread = agent.GetNewThread(); + _ = this.RunAsync(cancellationToken); + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + // Delay a little to allow the service to finish starting. + await Task.Delay(100, cancellationToken); + + while (cancellationToken.IsCancellationRequested is false) + { + Console.WriteLine("\nAgent: Ask me to tell you a joke about a specific topic. To exit just press Ctrl+C or enter without any input.\n"); + Console.Write("> "); + var input = Console.ReadLine(); + + // If the user enters no input, signal the application to shut down. + if (string.IsNullOrWhiteSpace(input)) + { + appShutdownCancellationTokenSource.Cancel(); + break; + } + + // Stream the output to the console as it is generated. + await foreach (var update in agent.RunStreamingAsync(input, this._thread!, cancellationToken: cancellationToken)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/dotnet/samples/GettingStartedSteps/Step09_ChatClientAgent_DependencyInjection/Step09_ChatClientAgent_DependencyInjection.csproj b/dotnet/samples/GettingStartedSteps/Step09_ChatClientAgent_DependencyInjection/Step09_ChatClientAgent_DependencyInjection.csproj new file mode 100644 index 0000000000..e2753ab403 --- /dev/null +++ b/dotnet/samples/GettingStartedSteps/Step09_ChatClientAgent_DependencyInjection/Step09_ChatClientAgent_DependencyInjection.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + + + diff --git a/dotnet/src/Shared/Demos/SampleEnvironment.cs b/dotnet/src/Shared/Demos/SampleEnvironment.cs index 37ae808f05..12b326bd69 100644 --- a/dotnet/src/Shared/Demos/SampleEnvironment.cs +++ b/dotnet/src/Shared/Demos/SampleEnvironment.cs @@ -35,7 +35,7 @@ internal static class SampleEnvironment Console.ForegroundColor = ConsoleColor.Yellow; Console.Write(key); Console.ForegroundColor = ConsoleColor.Green; - Console.Write("'> "); + Console.Write("'. Just press enter to accept the default. > "); Console.ForegroundColor = color; value = Console.ReadLine(); value = string.IsNullOrWhiteSpace(value) ? null : value.Trim();