From 636e3acf3741a2dc67f8481fa9eb8b2ea53c895d Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:39:38 +0100 Subject: [PATCH] .NET: [BREAKING] Change implementation to use AsFunctionTool extension method to create a function from an agent (#686) * Change implementation to use AsFunctionTool extension method to create a function from an agent * Update dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address code review feedback and add a new sample * Address some code review feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 1 + .../A2AClient/HostClientAgent.cs | 2 +- .../Agent_Step12_AsFunctionTool.csproj | 24 ++++++++++++ .../Agent_Step12_AsFunctionTool/Program.cs | 39 +++++++++++++++++++ .../samples/GettingStarted/Agents/README.md | 1 + .../AgentAIFunctionFactory.cs | 39 ------------------- .../AgentExtensions.cs | 31 +++++++++++++++ ...actoryTests.cs => AgentExtensionsTests.cs} | 36 ++++++++--------- 8 files changed, 115 insertions(+), 58 deletions(-) create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Program.cs delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents/AgentAIFunctionFactory.cs rename dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/{AgentAIFunctionFactoryTests.cs => AgentExtensionsTests.cs} (88%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index f7a78e5535..324d63cb19 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -49,6 +49,7 @@ + diff --git a/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs index db95f49af4..75513b0834 100644 --- a/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs +++ b/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs @@ -25,7 +25,7 @@ internal sealed class HostClientAgent // Connect to the remote agents via A2A var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl)); var agents = await Task.WhenAll(createAgentTasks); - var tools = agents.Select(agent => (AITool)AgentAIFunctionFactory.CreateFromAgent(agent)).ToList(); + var tools = agents.Select(agent => (AITool)agent.AsAIFunction()).ToList(); // Create the agent that uses the remote agents as tools this.Agent = new OpenAIClient(new ApiKeyCredential(apiKey)) diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj new file mode 100644 index 0000000000..84ef628db4 --- /dev/null +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + 12 + + enable + disable + 3afc9b74-af74-4d8e-ae96-fa1c511d11ac + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Program.cs new file mode 100644 index 0000000000..50feac82bd --- /dev/null +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Program.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a Azure OpenAI AI agent as a function tool. + +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 weatherAgent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent( + instructions: "You answer questions about the weather.", + name: "WeatherAgent", + description: "An agent that answers questions about the weather.", + tools: [AIFunctionFactory.Create(GetWeather)]); + +// Create the main agent, and provide the weather agent as a function tool. +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent(instructions: "You are a helpful assistant who responds in French.", tools: [weatherAgent.AsAIFunction()]); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?")); diff --git a/dotnet/samples/GettingStarted/Agents/README.md b/dotnet/samples/GettingStarted/Agents/README.md index c6959aee65..43da38e0ff 100644 --- a/dotnet/samples/GettingStarted/Agents/README.md +++ b/dotnet/samples/GettingStarted/Agents/README.md @@ -37,6 +37,7 @@ Before you begin, ensure you have the following prerequisites: |[Dependency injection with a simple agent](./Agent_Step09_DependencyInjection/)|This sample demonstrates how to add and resolve an agent with a dependency injection container| |[Exposing a simple agent as MCP tool](./Agent_Step10_AsMcpTool/)|This sample demonstrates how to expose an agent as an MCP tool| |[Using images with a simple agent](./Agent_Step11_UsingImages/)|This sample demonstrates how to use image multi-modality with an AI agent| +|[Exposing a simple agent a function tool](./Agent_Step12_AsFunctionTool/)|This sample demonstrates how to expose an agent as a function tool| ## Running the samples from the console diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/AgentAIFunctionFactory.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/AgentAIFunctionFactory.cs deleted file mode 100644 index 76f8f8fe12..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/AgentAIFunctionFactory.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Shared.Diagnostics; -using static Microsoft.Extensions.AI.Agents.OpenTelemetryConsts.GenAI; - -namespace Microsoft.Extensions.AI.Agents; - -/// -/// Provides factory methods for creating implementations of backed by an . -/// -public static class AgentAIFunctionFactory -{ - /// - /// Creates a that will invoke the provided Agent. - /// - /// The to be represented via the created . - /// Metadata to use to override defaults inferred from . - /// The created for invoking the . - public static AIFunction CreateFromAgent( - AIAgent agent, - AIFunctionFactoryOptions? options = null) - { - Throw.IfNull(agent); - - async Task RunAgentAsync(string query, CancellationToken cancellationToken) - { - var response = await agent.RunAsync(query, cancellationToken: cancellationToken).ConfigureAwait(false); - return response.Text; - } - - return AIFunctionFactory.Create(RunAgentAsync, options ?? new() - { - Name = agent.Name, - Description = agent.Description, - }); - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/AgentExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/AgentExtensions.cs index d45130ce38..4ceec57304 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/AgentExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/AgentExtensions.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Agents; @@ -24,4 +28,31 @@ public static class AgentExtensions EnableSensitiveData = enableSensitiveData ?? false }; } + + /// + /// Creates a that will invoke the provided Agent. + /// + /// The to be represented via the created . + /// Metadata to use to override defaults inferred from . + /// The to use for the function. + /// The created for invoking the . + public static AIFunction AsAIFunction(this AIAgent agent, AIFunctionFactoryOptions? options = null, AgentThread? thread = null) + { + Throw.IfNull(agent); + + [Description("Invoke an agent to retrieve some information.")] + async Task InvokeAgentAsync( + [Description("Input query to invoke the agent.")] string query, + CancellationToken cancellationToken) + { + var response = await agent.RunAsync(query, thread: thread, cancellationToken: cancellationToken).ConfigureAwait(false); + return response.Text; + } + + options ??= new(); + options.Name ??= agent.Name; + options.Description ??= agent.Description; + + return AIFunctionFactory.Create(InvokeAgentAsync, options); + } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentAIFunctionFactoryTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentExtensionsTests.cs similarity index 88% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentAIFunctionFactoryTests.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentExtensionsTests.cs index 014c9b6759..5292277f02 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentAIFunctionFactoryTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/AgentExtensionsTests.cs @@ -10,16 +10,16 @@ using Moq; namespace Microsoft.Extensions.AI.Agents.UnitTests; /// -/// Unit tests for the class. +/// Unit tests for the method. /// -public class AgentAIFunctionFactoryTests +public class AgentExtensionsTests { [Fact] public void CreateFromAgent_WithNullAgent_ThrowsArgumentNullException() { // Act & Assert var exception = Assert.Throws(() => - AgentAIFunctionFactory.CreateFromAgent(null!)); + AgentExtensions.AsAIFunction(null!)); Assert.Equal("agent", exception.ParamName); } @@ -33,7 +33,7 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns("Test agent description"); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object); + var result = mockAgent.Object.AsAIFunction(); // Assert Assert.NotNull(result); @@ -51,7 +51,7 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns("Test description"); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object); + var result = mockAgent.Object.AsAIFunction(); // Assert Assert.NotNull(result); @@ -60,7 +60,7 @@ public class AgentAIFunctionFactoryTests } [Fact] - public void CreateFromAgent_WithAgentHavingNullDescription_UsesEmptyDescription() + public void CreateFromAgent_WithAgentHavingNullDescription_UsesDefaultDescription() { // Arrange var mockAgent = new Mock(); @@ -68,12 +68,12 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns((string?)null); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object); + var result = mockAgent.Object.AsAIFunction(); // Assert Assert.NotNull(result); Assert.Equal("TestAgent", result.Name); - Assert.Equal(string.Empty, result.Description); + Assert.Equal("Invoke an agent to retrieve some information.", result.Description); } [Fact] @@ -91,7 +91,7 @@ public class AgentAIFunctionFactoryTests }; // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object, customOptions); + var result = mockAgent.Object.AsAIFunction(customOptions); // Assert Assert.NotNull(result); @@ -108,7 +108,7 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns("Test agent description"); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object, null); + var result = mockAgent.Object.AsAIFunction(null); // Assert Assert.NotNull(result); @@ -123,7 +123,7 @@ public class AgentAIFunctionFactoryTests var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Test response")); var testAgent = new TestAgent("TestAgent", "Test description", expectedResponse); - var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent); + var aiFunction = testAgent.AsAIFunction(); // Act var arguments = new AIFunctionArguments() { ["query"] = "Test query" }; @@ -143,7 +143,7 @@ public class AgentAIFunctionFactoryTests using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent); + var aiFunction = testAgent.AsAIFunction(); // Act var arguments = new AIFunctionArguments() { ["query"] = "Test query" }; @@ -161,7 +161,7 @@ public class AgentAIFunctionFactoryTests var expectedException = new InvalidOperationException("Test exception"); var testAgent = new TestAgent("TestAgent", "Test description", expectedException); - var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent); + var aiFunction = testAgent.AsAIFunction(); // Act & Assert var arguments = new AIFunctionArguments() { ["query"] = "Test query" }; @@ -180,7 +180,7 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns("Test description"); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object); + var result = mockAgent.Object.AsAIFunction(); // Assert Assert.NotNull(result); @@ -204,7 +204,7 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns("Test description"); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object); + var result = mockAgent.Object.AsAIFunction(); // Assert Assert.NotNull(result); @@ -221,7 +221,7 @@ public class AgentAIFunctionFactoryTests mockAgent.Setup(a => a.Description).Returns(string.Empty); // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object); + var result = mockAgent.Object.AsAIFunction(); // Assert Assert.NotNull(result); @@ -244,7 +244,7 @@ public class AgentAIFunctionFactoryTests }; // Act - var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object, customOptions); + var result = mockAgent.Object.AsAIFunction(customOptions); // Assert Assert.NotNull(result); @@ -265,7 +265,7 @@ public class AgentAIFunctionFactoryTests }; var testAgent = new TestAgent("TestAgent", "Test description", expectedResponse); - var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent); + var aiFunction = testAgent.AsAIFunction(); // Act var arguments = new AIFunctionArguments() { ["query"] = "Test query" };