.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>
This commit is contained in:
Mark Wallace
2025-09-15 09:39:38 +01:00
committed by GitHub
Unverified
parent 2a75482777
commit 636e3acf37
8 changed files with 115 additions and 58 deletions
+1
View File
@@ -49,6 +49,7 @@
<Project Path="samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
@@ -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))
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<UserSecretsId>3afc9b74-af74-4d8e-ae96-fa1c511d11ac</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -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?"));
@@ -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
@@ -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;
/// <summary>
/// Provides factory methods for creating implementations of <see cref="AIFunction"/> backed by an <see cref="AIAgent" />.
/// </summary>
public static class AgentAIFunctionFactory
{
/// <summary>
/// Creates a <see cref="AIFunction"/> that will invoke the provided Agent.
/// </summary>
/// <param name="agent">The <see cref="Agent" /> to be represented via the created <see cref="AIFunction"/>.</param>
/// <param name="options">Metadata to use to override defaults inferred from <paramref name="agent"/>.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking the <see cref="AIAgent"/>.</returns>
public static AIFunction CreateFromAgent(
AIAgent agent,
AIFunctionFactoryOptions? options = null)
{
Throw.IfNull(agent);
async Task<string> 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,
});
}
}
@@ -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
};
}
/// <summary>
/// Creates a <see cref="AIFunction"/> that will invoke the provided Agent.
/// </summary>
/// <param name="agent">The <see cref="AIAgent" /> to be represented via the created <see cref="AIFunction"/>.</param>
/// <param name="options">Metadata to use to override defaults inferred from <paramref name="agent"/>.</param>
/// <param name="thread">The <see cref="AgentThread"/> to use for the function.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking the <see cref="AIAgent"/>.</returns>
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<string> 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);
}
}
@@ -10,16 +10,16 @@ using Moq;
namespace Microsoft.Extensions.AI.Agents.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AgentAIFunctionFactory"/> class.
/// Unit tests for the <see cref="AgentExtensions.AsAIFunction"/> method.
/// </summary>
public class AgentAIFunctionFactoryTests
public class AgentExtensionsTests
{
[Fact]
public void CreateFromAgent_WithNullAgent_ThrowsArgumentNullException()
{
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
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<AIAgent>();
@@ -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" };