From 2c75f13337488b96eff0c480c353f7221bc851d6 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Thu, 12 Jun 2025 13:22:07 +0100
Subject: [PATCH] .Net: Add ChatClientAgent Samples - OpenAI Model Client (#72)
* Add Streaming API
* Removing InstructionsRole
* Updating thread notification strategy
* Fix net472 failing
* Small fixes
* Adding Samples for OpenAI
* WIP samples
* default runsettings for unit tests
* Adding first samples with OpenAIModelChatClientAgents
* Removing OpenAI dependency on the sample utility
* Release -> Debug update for GettingStarted project
* Fix GettingStarted.csproj failing to build in Release
* Update dotnet/src/Shared/Samples/BaseSample.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Address PR feedback
* Fix Step 1 samples
* Simplify code
* Address PR feedback
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
dotnet/Directory.Packages.props | 15 +-
dotnet/agent-framework-dotnet.slnx | 13 ++
dotnet/eng/MSBuild/Shared.props | 3 +
dotnet/samples/.editorconfig | 7 +
dotnet/samples/Directory.Build.props | 27 +++
dotnet/samples/GettingStarted/AgentSample.cs | 15 ++
.../ChatClientAgent/Step01_Running.cs | 125 ++++++++++
.../ChatClientAgent/Step02_UsingTools.cs | 132 +++++++++++
.../GettingStarted/GettingStarted.csproj | 41 ++++
.../ChatClientAgentExtensions.cs | 50 +++-
dotnet/src/Shared/Samples/BaseSample.cs | 215 ++++++++++++++++++
dotnet/src/Shared/Samples/README.md | 11 +
.../src/Shared/Samples/TestConfiguration.cs | 81 +++++++
.../Samples/TextOutputHelperExtensions.cs | 47 ++++
dotnet/src/Shared/Samples/XunitLogger.cs | 42 ++++
dotnet/src/Shared/Throw/Throw.cs | 2 +
dotnet/tests/.editorconfig | 2 +-
17 files changed, 822 insertions(+), 6 deletions(-)
create mode 100644 dotnet/samples/.editorconfig
create mode 100644 dotnet/samples/Directory.Build.props
create mode 100644 dotnet/samples/GettingStarted/AgentSample.cs
create mode 100644 dotnet/samples/GettingStarted/ChatClientAgent/Step01_Running.cs
create mode 100644 dotnet/samples/GettingStarted/ChatClientAgent/Step02_UsingTools.cs
create mode 100644 dotnet/samples/GettingStarted/GettingStarted.csproj
create mode 100644 dotnet/src/Shared/Samples/BaseSample.cs
create mode 100644 dotnet/src/Shared/Samples/README.md
create mode 100644 dotnet/src/Shared/Samples/TestConfiguration.cs
create mode 100644 dotnet/src/Shared/Samples/TextOutputHelperExtensions.cs
create mode 100644 dotnet/src/Shared/Samples/XunitLogger.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index a7313c7b0d..a295476d40 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -6,13 +6,20 @@
+
+
+
+
+
+
-
-
+
+
+
-
+
@@ -61,4 +68,4 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
\ No newline at end of file
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 8a13e5e860..c8775abc1e 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -4,12 +4,18 @@
+
+
+
+
+
+
@@ -20,15 +26,22 @@
+
+
+
+
+
+
+
diff --git a/dotnet/eng/MSBuild/Shared.props b/dotnet/eng/MSBuild/Shared.props
index fdd9e5a802..7d3e5ca30b 100644
--- a/dotnet/eng/MSBuild/Shared.props
+++ b/dotnet/eng/MSBuild/Shared.props
@@ -2,4 +2,7 @@
+
+
+
diff --git a/dotnet/samples/.editorconfig b/dotnet/samples/.editorconfig
new file mode 100644
index 0000000000..348c57a8b1
--- /dev/null
+++ b/dotnet/samples/.editorconfig
@@ -0,0 +1,7 @@
+# Suppressing errors for Sample projects under dotnet/samples folder
+[*.cs]
+dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
+dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
+dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
+dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
+dotnet_diagnostic.CA1716.severity = none # Add summary to documentation comment.
\ No newline at end of file
diff --git a/dotnet/samples/Directory.Build.props b/dotnet/samples/Directory.Build.props
new file mode 100644
index 0000000000..e5c47346d2
--- /dev/null
+++ b/dotnet/samples/Directory.Build.props
@@ -0,0 +1,27 @@
+
+
+
+
+
+ false
+ true
+ false
+ net472;net9.0
+ net9.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentSample.cs b/dotnet/samples/GettingStarted/AgentSample.cs
new file mode 100644
index 0000000000..3ba34ac3fa
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentSample.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Samples;
+using OpenAI;
+
+namespace GettingStarted;
+
+public class AgentSample(ITestOutputHelper output) : BaseSample(output)
+{
+ protected IChatClient GetOpenAIChatClient()
+ => new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
+ .GetChatClient(TestConfiguration.OpenAI.ChatModelId)
+ .AsIChatClient();
+}
diff --git a/dotnet/samples/GettingStarted/ChatClientAgent/Step01_Running.cs b/dotnet/samples/GettingStarted/ChatClientAgent/Step01_Running.cs
new file mode 100644
index 0000000000..29df4658cf
--- /dev/null
+++ b/dotnet/samples/GettingStarted/ChatClientAgent/Step01_Running.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents;
+
+namespace ChatCompletionAgent;
+
+///
+/// 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_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.
+ ///
+ [Fact]
+ public async Task RunWithoutThread()
+ {
+ // Get the chat client to use for the agent.
+ using var chatClient = base.GetOpenAIChatClient();
+
+ // Define the agent
+ ChatClientAgent agent =
+ new(chatClient, new()
+ {
+ Name = ParrotName,
+ Instructions = ParrotInstructions,
+ });
+
+ // Respond to user input
+ await InvokeAgentAsync("Fortune favors the bold.");
+ await InvokeAgentAsync("I came, I saw, I conquered.");
+ await InvokeAgentAsync("Practice makes perfect.");
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(string input)
+ {
+ this.WriteUserMessage(input);
+
+ var response = await agent.RunAsync(input);
+ this.WriteResponseOutput(response);
+ }
+ }
+
+ ///
+ /// Demonstrate the usage of where a conversation history is maintained.
+ ///
+ [Fact]
+ public async Task RunWithConversationThread()
+ {
+ // Get the chat client to use for the agent.
+ using var chatClient = base.GetOpenAIChatClient();
+
+ // Define the agent
+ ChatClientAgent agent =
+ new(chatClient, new()
+ {
+ Name = JokerName,
+ Instructions = JokerInstructions,
+ });
+
+ // Start a new thread for the agent conversation.
+ AgentThread thread = agent.GetNewThread();
+
+ // Respond to user input
+ await InvokeAgentAsync("Tell me a joke about a pirate.");
+ await InvokeAgentAsync("Now add some emojis to the joke.");
+
+ // Local function to invoke agent and display the conversation messages for the thread.
+ async Task InvokeAgentAsync(string input)
+ {
+ this.WriteUserMessage(input);
+
+ var response = await agent.RunAsync(input, thread);
+
+ this.WriteResponseOutput(response);
+ }
+ }
+
+ ///
+ /// Demonstrate the usage of in streaming mode,
+ /// where a conversation is maintained by the .
+ ///
+ [Fact]
+ public async Task StreamingRunWithConversationThread()
+ {
+ // Get the chat client to use for the agent.
+ using var chatClient = base.GetOpenAIChatClient();
+
+ // Define the agent
+ ChatClientAgent agent =
+ new(chatClient, new()
+ {
+ Name = ParrotName,
+ Instructions = ParrotInstructions,
+ });
+
+ // Start a new thread for the agent conversation.
+ AgentThread thread = agent.GetNewThread();
+
+ // Respond to user input
+ await InvokeAgentAsync("Tell me a joke about a pirate.");
+ await InvokeAgentAsync("Now add some emojis to the joke.");
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(string input)
+ {
+ this.WriteUserMessage(input);
+
+ await foreach (var update in agent.RunStreamingAsync(input, thread))
+ {
+ this.WriteAgentOutput(update);
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/ChatClientAgent/Step02_UsingTools.cs b/dotnet/samples/GettingStarted/ChatClientAgent/Step02_UsingTools.cs
new file mode 100644
index 0000000000..56dc8e3d55
--- /dev/null
+++ b/dotnet/samples/GettingStarted/ChatClientAgent/Step02_UsingTools.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using Microsoft.Agents;
+using Microsoft.Extensions.AI;
+
+namespace ChatCompletionAgent;
+
+public sealed class Step02_UsingTools(ITestOutputHelper output) : AgentSample(output)
+{
+ [Fact]
+ public async Task RunningWithTools()
+ {
+ // Get the chat client to use for the agent.
+ using var chatClient = base.GetOpenAIChatClient();
+
+ // Define the agent
+ ChatClientAgent agent =
+ new(chatClient, new()
+ {
+ Name = "Host",
+ Instructions = "Answer questions about the menu.",
+ });
+
+ var menuTools = new MenuTools();
+ var chatOptions = new ChatOptions
+ {
+ Tools = [
+ AIFunctionFactory.Create(menuTools.GetMenu),
+ AIFunctionFactory.Create(menuTools.GetSpecials),
+ AIFunctionFactory.Create(menuTools.GetItemPrice),
+ ],
+ };
+
+ // Create the chat history thread to capture the agent interaction.
+ var thread = agent.GetNewThread();
+
+ // Respond to user input, invoking functions where appropriate.
+ await InvokeAgentAsync("Hello");
+ await InvokeAgentAsync("What is the special soup and its price?");
+ await InvokeAgentAsync("What is the special drink and its price?");
+ await InvokeAgentAsync("Thank you");
+
+ async Task InvokeAgentAsync(string input)
+ {
+ this.WriteUserMessage(input);
+ var response = await agent.RunAsync(input, thread, chatOptions: chatOptions);
+ this.WriteResponseOutput(response);
+ }
+ }
+
+ [Fact]
+ public async Task StreamingRunWithTools()
+ {
+ // Get the chat client to use for the agent.
+ using var chatClient = base.GetOpenAIChatClient();
+
+ // Define the agent
+ ChatClientAgent agent =
+ new(chatClient, new()
+ {
+ Name = "Host",
+ Instructions = "Answer questions about the menu.",
+ });
+
+ var menuTools = new MenuTools();
+ var chatOptions = new ChatOptions
+ {
+ Tools = [
+ AIFunctionFactory.Create(menuTools.GetMenu),
+ AIFunctionFactory.Create(menuTools.GetSpecials),
+ AIFunctionFactory.Create(menuTools.GetItemPrice),
+ ],
+ };
+
+ // Create the chat history thread to capture the agent interaction.
+ var thread = agent.GetNewThread();
+
+ // Respond to user input, invoking functions where appropriate.
+ await InvokeAgentAsync("Hello");
+ await InvokeAgentAsync("What is the special soup and its price?");
+ await InvokeAgentAsync("What is the special drink and its price?");
+ await InvokeAgentAsync("Thank you");
+
+ async Task InvokeAgentAsync(string input)
+ {
+ this.WriteUserMessage(input);
+ await foreach (var update in agent.RunStreamingAsync(input, thread, chatOptions: chatOptions))
+ {
+ this.WriteAgentOutput(update);
+ }
+ }
+ }
+
+ 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