.NET: AzureAI Package - Skip tool validation when UseProvidedChatClientAsIs is true (#4389)

* Skip tool validation when UseProvidedChatClientAsIs is true (#3855)

When GetAIAgentAsync is called with ChatClientAgentOptions.UseProvidedChatClientAsIs = true,
skip requireInvocableTools validation so users can handle function calls manually
via custom ChatClient middleware without needing to provide matching AIFunction tools.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify requireInvocableTools expression per review feedback

UseProvidedChatClientAsIs is a non-nullable bool, so use ! operator
instead of != true for clarity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Decouple tool matching from validation and add tool preservation test (#3855)

Always match provided AIFunctions to server-side function definitions
regardless of requireInvocableTools flag. Only throw when validation
is required and no match is found. This ensures UseProvidedChatClientAsIs
still preserves user-provided AIFunction tools instead of falling back
to the broken ResponseToolAITool wrapper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Roger Barreto
2026-03-02 22:12:55 +00:00
committed by GitHub
Unverified
parent 3b4eed270f
commit a442ee115d
2 changed files with 75 additions and 9 deletions
@@ -191,7 +191,7 @@ public static partial class AzureAIProjectChatClientExtensions
AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, options.Name, cancellationToken).ConfigureAwait(false);
var agentVersion = agentRecord.Versions.Latest;
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true);
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: !options.UseProvidedChatClientAsIs);
return AsChatClientAgent(
aiProjectClient,
@@ -522,21 +522,23 @@ public static partial class AzureAIProjectChatClientExtensions
// Check function tools
foreach (ResponseTool responseTool in definitionTools)
{
if (requireInvocableTools && responseTool is FunctionTool functionTool)
if (responseTool is FunctionTool functionTool)
{
// Check if a tool with the same type and name exists in the provided tools.
// When invocable tools are required, match only AIFunction.
// Always prefer matching AIFunction when available, regardless of requireInvocableTools.
var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name);
if (matchingTool is null)
{
(missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
}
else
if (matchingTool is not null)
{
(agentTools ??= []).Add(matchingTool!);
continue;
}
if (requireInvocableTools)
{
(missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
continue;
}
continue;
}
(agentTools ??= []).Add(responseTool.AsAITool());
@@ -2375,6 +2375,70 @@ public sealed class AzureAIProjectChatClientExtensionsTests
Assert.NotNull(agent);
}
/// <summary>
/// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true skips tool validation
/// and does not throw even when server-side function tools exist without matching invocable tools.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_SkipsToolValidationAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" },
UseProvidedChatClientAsIs = true
};
// Act - should not throw even without tools when UseProvidedChatClientAsIs is true
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
}
/// <summary>
/// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true still matches provided AIFunction tools
/// to server-side function definitions, instead of falling back to the ResponseToolAITool wrapper.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesProvidedToolsAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("my_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var providedTool = AIFunctionFactory.Create(() => "test", "my_function", "A test function");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
UseProvidedChatClientAsIs = true,
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = [providedTool]
},
};
// Act - UseProvidedChatClientAsIs is true, but provided AIFunctions should still be matched and preserved
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
// Verify the provided AIFunction was matched and preserved in ChatOptions.Tools (not replaced by AsAITool wrapper)
var chatOptions = agent.GetService<ChatOptions>();
Assert.NotNull(chatOptions);
Assert.NotNull(chatOptions!.Tools);
Assert.Contains(chatOptions.Tools, t => t is AIFunction af && af.Name == "my_function");
}
#endregion
#region Empty Version and ID Handling Tests