.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.
This commit is contained in:
westey
2025-10-07 10:47:33 +01:00
committed by GitHub
Unverified
parent 74905af778
commit 3dc14dac79
7 changed files with 452 additions and 20 deletions
@@ -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."));
@@ -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."));
@@ -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);
@@ -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;
/// <summary>
/// Provides extension methods for building a <see cref="ChatClientAgent"/> from a <see cref="ChatClientBuilder"/>.
/// </summary>
public static class ChatClientBuilderExtensions
{
/// <summary>
/// Build a <see cref="ChatClientAgent"/> from the <see cref="IChatClient"/> pipeline described by this <see cref="ChatClientBuilder"/>.
/// </summary>
/// <param name="builder">A builder for creating pipelines of <see cref="IChatClient"/>.</param>
/// <param name="instructions">
/// Optional system instructions that guide the agent's behavior. These instructions are provided to the <see cref="IChatClient"/>
/// with each invocation to establish the agent's role and behavior.
/// </param>
/// <param name="name">
/// Optional name for the agent. This name is used for identification and logging purposes.
/// </param>
/// <param name="description">
/// Optional human-readable description of the agent's purpose and capabilities.
/// This description can be useful for documentation and agent discovery scenarios.
/// </param>
/// <param name="tools">
/// Optional collection of tools that the agent can invoke during conversations.
/// These tools augment any tools that may be provided to the agent via <see cref="ChatOptions.Tools"/> when
/// the agent is run.
/// </param>
/// <param name="loggerFactory">
/// Optional logger factory for creating loggers used by the agent and its components.
/// </param>
/// <param name="services">
/// 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.
/// </param>
/// <returns>A new <see cref="ChatClientAgent"/> instance.</returns>
public static ChatClientAgent BuildAIAgent(
this ChatClientBuilder builder,
string? instructions = null,
string? name = null,
string? description = null,
IList<AITool>? 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);
/// <summary>
/// Creates a new <see cref="ChatClientAgent"/> instance.
/// </summary>
/// <param name="builder">A builder for creating pipelines of <see cref="IChatClient"/>.</param>
/// <param name="options">
/// Configuration options that control all aspects of the agent's behavior, including chat settings,
/// message store factories, context provider factories, and other advanced configurations.
/// </param>
/// <param name="loggerFactory">
/// Optional logger factory for creating loggers used by the agent and its components.
/// </param>
/// <param name="services">
/// 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.
/// </param>
/// <returns>A new <see cref="ChatClientAgent"/> instance.</returns>
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);
}
@@ -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
/// <summary>
/// Provides extension methods for Creating an <see cref="AIAgent"/> from an <see cref="IChatClient"/>.
/// </summary>
public static class ChatClientExtensions
{
/// <summary>
/// Creates a new <see cref="ChatClientAgent"/> instance.
/// </summary>
/// <inheritdoc cref="ChatClientAgent(IChatClient, string?, string?, string?, IList{AITool}?, ILoggerFactory?, IServiceProvider?)"/>
/// <returns>A new <see cref="ChatClientAgent"/> instance.</returns>
public static ChatClientAgent CreateAIAgent(
this IChatClient chatClient,
string? instructions = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null) =>
new(
chatClient,
instructions: instructions,
name: name,
description: description,
tools: tools,
loggerFactory: loggerFactory,
services: services);
/// <summary>
/// Creates a new <see cref="ChatClientAgent"/> instance.
/// </summary>
/// <inheritdoc cref="ChatClientAgent(IChatClient, ChatClientAgentOptions?, ILoggerFactory?, IServiceProvider?)"/>
/// <returns>A new <see cref="ChatClientAgent"/> instance.</returns>
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();
@@ -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;
/// <summary>
/// Contains unit tests for the <see cref="ChatClientBuilderExtensions"/> class.
/// </summary>
public sealed class ChatClientBuilderExtensionsTests
{
[Fact]
public void BuildAIAgent_WithBasicParameters_CreatesAgent()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
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<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var tools = new List<AITool> { new Mock<AITool>().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<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var tools = new List<AITool> { new Mock<AITool>().Object };
var loggerFactoryMock = new Mock<ILoggerFactory>();
var serviceProviderMock = new Mock<IServiceProvider>();
// 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<IChatClient>();
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<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var loggerFactoryMock = new Mock<ILoggerFactory>();
var serviceProviderMock = new Mock<IServiceProvider>();
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<ArgumentNullException>(() => builder.BuildAIAgent(instructions: "instructions"));
}
[Fact]
public void BuildAIAgent_WithNullBuilderAndOptions_Throws()
{
// Arrange
ChatClientBuilder builder = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(options: new() { Instructions = "instructions" }));
}
[Fact]
public void BuildAIAgent_WithMiddleware_BuildsCorrectPipeline()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var middlewareChatClientMock = new Mock<IChatClient>();
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<IChatClient>();
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<IChatClient>();
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);
}
}
@@ -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;
/// <summary>
/// Contains unit tests for the ChatClientExtensions class.
/// </summary>
public sealed class ChatClientExtensionsTests
{
[Fact]
public void CreateAIAgent_WithBasicParameters_CreatesAgent()
{
// Arrange
var chatClientMock = new Mock<IChatClient>();
// 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<IChatClient>();
var tools = new List<AITool> { new Mock<AITool>().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<IChatClient>();
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<ArgumentNullException>(() => chatClient.CreateAIAgent(instructions: "instructions"));
}
[Fact]
public void CreateAIAgent_WithNullClientAndOptions_Throws()
{
// Arrange
IChatClient chatClient = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => chatClient.CreateAIAgent(options: new() { Instructions = "instructions" }));
}
}