From 3dc14dac79410ab407e2c21843e84b0a2958c73d Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Tue, 7 Oct 2025 10:47:33 +0100
Subject: [PATCH] .NET: Add a CreateAIAgent extension method to IChatClient
(#1223)
* Add a CreateAIAgent extension method to IChatClient
* Fix unit tests
* Add tests with null chat clients to ensure appropriate failure
* Switch to similar constructor
* Add ChatClientBuilder BuildAgent extensions
* Address code review comments.
---
.../AgentProviders/Agent_With_ONNX/Program.cs | 3 +-
.../Agent_With_Ollama/Program.cs | 5 +-
.../Agents/Agent_Step14_Middleware/Program.cs | 29 ++-
.../ChatClient/ChatClientBuilderExtensions.cs | 85 +++++++
.../ChatClient/ChatClientExtensions.cs | 40 +++-
.../ChatClientBuilderExtensionsTests.cs | 216 ++++++++++++++++++
.../ChatClient/ChatClientExtensionsTests.cs | 94 ++++++++
7 files changed, 452 insertions(+), 20 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Program.cs
index aed123899c..b8db89d2ef 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Program.cs
@@ -4,6 +4,7 @@
// WARNING: ONNX doesn't support function calling, so any function tools passed to the agent will be ignored.
using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
using Microsoft.ML.OnnxRuntimeGenAI;
// E.g. C:\repos\Phi-4-mini-instruct-onnx\cpu_and_mobile\cpu-int4-rtn-block-32-acc-level-4
@@ -14,7 +15,7 @@ const string JokerInstructions = "You are good at telling jokes.";
// Get a chat client for ONNX and use it to construct an AIAgent.
using OnnxRuntimeGenAIChatClient chatClient = new(modelPath);
-AIAgent agent = new ChatClientAgent(chatClient, JokerInstructions, JokerName);
+AIAgent agent = chatClient.CreateAIAgent(JokerInstructions, JokerName);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Program.cs
index 6802a8c590..8ba07cd634 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Program.cs
@@ -3,6 +3,7 @@
// This sample shows how to create and use a simple AI agent with Ollama as the backend.
using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
using OllamaSharp;
var endpoint = Environment.GetEnvironmentVariable("OLLAMA_ENDPOINT") ?? throw new InvalidOperationException("OLLAMA_ENDPOINT is not set.");
@@ -12,8 +13,8 @@ const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Get a chat client for Ollama and use it to construct an AIAgent.
-using OllamaApiClient chatClient = new(new Uri(endpoint), modelName);
-AIAgent agent = new ChatClientAgent(chatClient, JokerInstructions, JokerName);
+AIAgent agent = new OllamaApiClient(new Uri(endpoint), modelName)
+ .CreateAIAgent(JokerInstructions, JokerName);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs
index 096553bf91..ea59c84ba4 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs
@@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.ChatClient;
using Microsoft.Extensions.AI;
// Get Azure AI Foundry configuration from environment variables
@@ -28,25 +29,21 @@ static string GetWeather([Description("The location to get the weather for.")] s
static string GetDateTime()
=> DateTimeOffset.Now.ToString();
-// Adding middleware to the chat client level
-var chatClient = azureOpenAIClient.AsIChatClient()
+// Adding middleware to the chat client level and building an agent on top of it
+var originalAgent = azureOpenAIClient.AsIChatClient()
.AsBuilder()
- .Use(getResponseFunc: ChatClientMiddleware, getStreamingResponseFunc: null)
- .Build();
-
-// For flexibility we create the agent without any middleware.
-var originalAgent = new ChatClientAgent(chatClient, new ChatClientAgentOptions(
+ .Use(getResponseFunc: ChatClientMiddleware, getStreamingResponseFunc: null)
+ .BuildAIAgent(
instructions: "You are an AI assistant that helps people find information.",
- // Agent level tools
- tools: [AIFunctionFactory.Create(GetDateTime, name: nameof(GetDateTime))]));
+ tools: [AIFunctionFactory.Create(GetDateTime, name: nameof(GetDateTime))]);
// Adding middleware to the agent level
var middlewareEnabledAgent = originalAgent
.AsBuilder()
- .Use(FunctionCallMiddleware)
- .Use(FunctionCallOverrideWeather)
- .Use(PIIMiddleware, null)
- .Use(GuardrailMiddleware, null)
+ .Use(FunctionCallMiddleware)
+ .Use(FunctionCallOverrideWeather)
+ .Use(PIIMiddleware, null)
+ .Use(GuardrailMiddleware, null)
.Build();
var thread = middlewareEnabledAgent.GetNewThread();
@@ -83,15 +80,15 @@ var optionsWithApproval = new ChatClientAgentRunOptions(new()
{
ChatClientFactory = (chatClient) => chatClient
.AsBuilder()
- .Use(PerRequestChatClientMiddleware, null) // Using the non-streaming for handling streaming as well
+ .Use(PerRequestChatClientMiddleware, null) // Using the non-streaming for handling streaming as well
.Build()
};
// var response = middlewareAgent // Using per-request middleware pipeline in addition to existing agent-level middleware
var response = await originalAgent // Using per-request middleware pipeline without existing agent-level middleware
.AsBuilder()
- .Use(PerRequestFunctionCallingMiddleware)
- .Use(ConsolePromptingApprovalMiddleware, null)
+ .Use(PerRequestFunctionCallingMiddleware)
+ .Use(ConsolePromptingApprovalMiddleware, null)
.Build()
.RunAsync("What's the current time and the weather in Seattle?", thread, optionsWithApproval);
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs
new file mode 100644
index 0000000000..124f66760e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.ChatClient;
+
+///
+/// Provides extension methods for building a from a .
+///
+public static class ChatClientBuilderExtensions
+{
+ ///
+ /// Build a from the pipeline described by this .
+ ///
+ /// A builder for creating pipelines of .
+ ///
+ /// Optional system instructions that guide the agent's behavior. These instructions are provided to the
+ /// with each invocation to establish the agent's role and behavior.
+ ///
+ ///
+ /// Optional name for the agent. This name is used for identification and logging purposes.
+ ///
+ ///
+ /// Optional human-readable description of the agent's purpose and capabilities.
+ /// This description can be useful for documentation and agent discovery scenarios.
+ ///
+ ///
+ /// Optional collection of tools that the agent can invoke during conversations.
+ /// These tools augment any tools that may be provided to the agent via when
+ /// the agent is run.
+ ///
+ ///
+ /// Optional logger factory for creating loggers used by the agent and its components.
+ ///
+ ///
+ /// Optional service provider for resolving dependencies required by AI functions and other agent components.
+ /// This is particularly important when using custom tools that require dependency injection.
+ ///
+ /// A new instance.
+ public static ChatClientAgent BuildAIAgent(
+ this ChatClientBuilder builder,
+ string? instructions = null,
+ string? name = null,
+ string? description = null,
+ IList? tools = null,
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null) =>
+ Throw.IfNull(builder).Build(services).CreateAIAgent(
+ instructions: instructions,
+ name: name,
+ description: description,
+ tools: tools,
+ loggerFactory: loggerFactory,
+ services: services);
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// A builder for creating pipelines of .
+ ///
+ /// Configuration options that control all aspects of the agent's behavior, including chat settings,
+ /// message store factories, context provider factories, and other advanced configurations.
+ ///
+ ///
+ /// Optional logger factory for creating loggers used by the agent and its components.
+ ///
+ ///
+ /// Optional service provider for resolving dependencies required by AI functions and other agent components.
+ /// This is particularly important when using custom tools that require dependency injection.
+ ///
+ /// A new instance.
+ public static ChatClientAgent BuildAIAgent(
+ this ChatClientBuilder builder,
+ ChatClientAgentOptions? options,
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null) =>
+ Throw.IfNull(builder).Build(services).CreateAIAgent(
+ options: options,
+ loggerFactory: loggerFactory,
+ services: services);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs
index 5b4dc016cd..f65d41efe7 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -9,8 +10,45 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.AI;
-internal static class ChatClientExtensions
+///
+/// Provides extension methods for Creating an from an .
+///
+public static class ChatClientExtensions
{
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// A new instance.
+ public static ChatClientAgent CreateAIAgent(
+ this IChatClient chatClient,
+ string? instructions = null,
+ string? name = null,
+ string? description = null,
+ IList? tools = null,
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null) =>
+ new(
+ chatClient,
+ instructions: instructions,
+ name: name,
+ description: description,
+ tools: tools,
+ loggerFactory: loggerFactory,
+ services: services);
+
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// A new instance.
+ public static ChatClientAgent CreateAIAgent(
+ this IChatClient chatClient,
+ ChatClientAgentOptions? options,
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null) =>
+ new(chatClient, options, loggerFactory, services);
+
internal static IChatClient WithDefaultAgentMiddleware(this IChatClient chatClient, ChatClientAgentOptions? options, IServiceProvider? services = null)
{
var chatBuilder = chatClient.AsBuilder();
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..cbcaaf94de
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.ChatClient;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace Microsoft.Agents.AI.UnitTests;
+
+///
+/// Contains unit tests for the class.
+///
+public sealed class ChatClientBuilderExtensionsTests
+{
+ [Fact]
+ public void BuildAIAgent_WithBasicParameters_CreatesAgent()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+
+ // Act
+ var agent = builder.BuildAIAgent(
+ instructions: "Test instructions",
+ name: "TestAgent",
+ description: "Test description"
+ );
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("TestAgent", agent.Name);
+ Assert.Equal("Test description", agent.Description);
+ Assert.Equal("Test instructions", agent.Instructions);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithTools_SetsToolsInOptions()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+ var tools = new List { new Mock().Object };
+
+ // Act
+ var agent = builder.BuildAIAgent(tools: tools);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.ChatOptions);
+ Assert.Equal(tools, agent.ChatOptions.Tools);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithAllParameters_CreatesAgentCorrectly()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+ var tools = new List { new Mock().Object };
+ var loggerFactoryMock = new Mock();
+ var serviceProviderMock = new Mock();
+
+ // Act
+ var agent = builder.BuildAIAgent(
+ instructions: "Complex instructions",
+ name: "ComplexAgent",
+ description: "Complex description",
+ tools: tools,
+ loggerFactory: loggerFactoryMock.Object,
+ services: serviceProviderMock.Object
+ );
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("ComplexAgent", agent.Name);
+ Assert.Equal("Complex description", agent.Description);
+ Assert.Equal("Complex instructions", agent.Instructions);
+ Assert.NotNull(agent.ChatOptions);
+ Assert.Equal(tools, agent.ChatOptions.Tools);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithOptions_CreatesAgentWithOptions()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+ var options = new ChatClientAgentOptions
+ {
+ Name = "AgentWithOptions",
+ Description = "Desc",
+ Instructions = "Instr",
+ UseProvidedChatClientAsIs = true
+ };
+
+ // Act
+ var agent = builder.BuildAIAgent(options);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("AgentWithOptions", agent.Name);
+ Assert.Equal("Desc", agent.Description);
+ Assert.Equal("Instr", agent.Instructions);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithOptionsAndServices_CreatesAgentCorrectly()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+ var loggerFactoryMock = new Mock();
+ var serviceProviderMock = new Mock();
+ var options = new ChatClientAgentOptions
+ {
+ Name = "ServiceAgent",
+ Instructions = "Service instructions"
+ };
+
+ // Act
+ var agent = builder.BuildAIAgent(
+ options: options,
+ loggerFactory: loggerFactoryMock.Object,
+ services: serviceProviderMock.Object
+ );
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("ServiceAgent", agent.Name);
+ Assert.Equal("Service instructions", agent.Instructions);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithNullBuilder_Throws()
+ {
+ // Arrange
+ ChatClientBuilder builder = null!;
+
+ // Act & Assert
+ Assert.Throws(() => builder.BuildAIAgent(instructions: "instructions"));
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithNullBuilderAndOptions_Throws()
+ {
+ // Arrange
+ ChatClientBuilder builder = null!;
+
+ // Act & Assert
+ Assert.Throws(() => builder.BuildAIAgent(options: new() { Instructions = "instructions" }));
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithMiddleware_BuildsCorrectPipeline()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var middlewareChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+
+ // Add middleware that returns our mock
+ builder.Use((client, services) => middlewareChatClientMock.Object);
+
+ // Act
+ var agent = builder.BuildAIAgent(
+ new ChatClientAgentOptions
+ {
+ Instructions = "Middleware test",
+ UseProvidedChatClientAsIs = true
+ }
+ );
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("Middleware test", agent.Instructions);
+ // When UseProvidedChatClientAsIs is true, the agent should use the middleware chat client directly
+ Assert.Same(middlewareChatClientMock.Object, agent.ChatClient);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithNullOptions_CreatesAgentWithDefaults()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+
+ // Act
+ var agent = builder.BuildAIAgent(options: null);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Null(agent.Name);
+ Assert.Null(agent.Description);
+ Assert.Null(agent.Instructions);
+ }
+
+ [Fact]
+ public void BuildAIAgent_WithEmptyParameters_CreatesMinimalAgent()
+ {
+ // Arrange
+ var innerChatClientMock = new Mock();
+ var builder = new ChatClientBuilder(innerChatClientMock.Object);
+
+ // Act
+ var agent = builder.BuildAIAgent();
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Null(agent.Name);
+ Assert.Null(agent.Description);
+ Assert.Null(agent.Instructions);
+ Assert.Null(agent.ChatOptions);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs
new file mode 100644
index 0000000000..182de0be5b
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.UnitTests;
+
+///
+/// Contains unit tests for the ChatClientExtensions class.
+///
+public sealed class ChatClientExtensionsTests
+{
+ [Fact]
+ public void CreateAIAgent_WithBasicParameters_CreatesAgent()
+ {
+ // Arrange
+ var chatClientMock = new Mock();
+
+ // Act
+ var agent = chatClientMock.Object.CreateAIAgent(
+ instructions: "Test instructions",
+ name: "TestAgent",
+ description: "Test description"
+ );
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("TestAgent", agent.Name);
+ Assert.Equal("Test description", agent.Description);
+ Assert.Equal("Test instructions", agent.Instructions);
+ }
+
+ [Fact]
+ public void CreateAIAgent_WithTools_SetsToolsInOptions()
+ {
+ // Arrange
+ var chatClientMock = new Mock();
+ var tools = new List { new Mock().Object };
+
+ // Act
+ var agent = chatClientMock.Object.CreateAIAgent(tools: tools);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.ChatOptions);
+ Assert.Equal(tools, agent.ChatOptions.Tools);
+ }
+
+ [Fact]
+ public void CreateAIAgent_WithOptions_CreatesAgentWithOptions()
+ {
+ // Arrange
+ var chatClientMock = new Mock();
+ var options = new ChatClientAgentOptions
+ {
+ Name = "AgentWithOptions",
+ Description = "Desc",
+ Instructions = "Instr",
+ UseProvidedChatClientAsIs = true
+ };
+
+ // Act
+ var agent = chatClientMock.Object.CreateAIAgent(options);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("AgentWithOptions", agent.Name);
+ Assert.Equal("Desc", agent.Description);
+ Assert.Equal("Instr", agent.Instructions);
+ Assert.Same(chatClientMock.Object, agent.ChatClient);
+ }
+
+ [Fact]
+ public void CreateAIAgent_WithNullClient_Throws()
+ {
+ // Arrange
+ IChatClient chatClient = null!;
+
+ // Act & Assert
+ Assert.Throws(() => chatClient.CreateAIAgent(instructions: "instructions"));
+ }
+
+ [Fact]
+ public void CreateAIAgent_WithNullClientAndOptions_Throws()
+ {
+ // Arrange
+ IChatClient chatClient = null!;
+
+ // Act & Assert
+ Assert.Throws(() => chatClient.CreateAIAgent(options: new() { Instructions = "instructions" }));
+ }
+}