From 7d233b9bc8081731d11648f8e30795a0301c5076 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 15 May 2026 17:44:46 +0100 Subject: [PATCH] Address PR review: forward pipeline settings; add UTs - CreateProjectClientOptions also carries RetryPolicy, NetworkTimeout, ClientLoggingOptions, MessageLoggingPolicy (was Transport+UserAgentApplicationId only). - Make CreateProjectClientOptions internal so tests can verify the copy directly. - Add AsAIAgent(Uri) UTs covering tools forwarding to inner ChatOptions and null tools handling. - Add CreateProjectClientOptions UTs covering null caller and full pipeline-settings copy. --- .../FoundryAgent.cs | 17 ++++- ...AzureAIProjectChatClientExtensionsTests.cs | 46 ++++++++++++ .../FoundryAgentTests.cs | 72 ++++++++++++++++++- 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs index 816d56302a..30bfc84d9a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs @@ -428,18 +428,31 @@ public sealed class FoundryAgent : DelegatingAIAgent return new AIProjectClient(endpoint, credential, clientOptions); } - private static AIProjectClientOptions? CreateProjectClientOptions(ProjectOpenAIClientOptions? clientOptions) + internal static AIProjectClientOptions? CreateProjectClientOptions(ProjectOpenAIClientOptions? clientOptions) { if (clientOptions is null) { return null; } - return new AIProjectClientOptions + // Copy pipeline behavior the caller configured on the per-agent options bag onto the + // project-level options bag so the agent endpoint client honors it. UserAgentApplicationId + // is project-level (not derived from the agent endpoint), so it must be carried through too. + var projectOptions = new AIProjectClientOptions { Transport = clientOptions.Transport, + RetryPolicy = clientOptions.RetryPolicy, + NetworkTimeout = clientOptions.NetworkTimeout, + MessageLoggingPolicy = clientOptions.MessageLoggingPolicy, UserAgentApplicationId = clientOptions.UserAgentApplicationId, }; + + if (clientOptions.ClientLoggingOptions is not null) + { + projectOptions.ClientLoggingOptions = clientOptions.ClientLoggingOptions; + } + + return projectOptions; } #endregion diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs index 7b01f8a79b..2996be725d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -1459,6 +1459,52 @@ public sealed class AzureAIProjectChatClientExtensionsTests Assert.Same(testChatClient, retrievedTestClient); } + /// + /// Verify that AsAIAgent(Uri agentEndpoint) forwards the supplied tools to the inner + /// 's . + /// + [Fact] + public void AsAIAgent_WithAgentEndpoint_ForwardsToolsToInnerChatOptions() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var tool1 = AIFunctionFactory.Create(() => "result-1", "tool_1", "First test tool."); + var tool2 = AIFunctionFactory.Create(() => "result-2", "tool_2", "Second test tool."); + List tools = [tool1, tool2]; + + // Act + var agent = client.AsAIAgent(new Uri(TestAgentEndpointUrl), tools: tools); + + // Assert + Assert.NotNull(agent); + ChatOptions? chatOptions = GetAgentChatOptions(agent); + Assert.NotNull(chatOptions); + Assert.NotNull(chatOptions!.Tools); + Assert.Equal(2, chatOptions.Tools!.Count); + Assert.Same(tool1, chatOptions.Tools[0]); + Assert.Same(tool2, chatOptions.Tools[1]); + } + + /// + /// Verify that AsAIAgent(Uri agentEndpoint) accepts a null tools argument without throwing + /// and produces an agent whose inner is null. + /// + [Fact] + public void AsAIAgent_WithAgentEndpoint_WithNullTools_DoesNotThrow() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + + // Act + var agent = client.AsAIAgent(new Uri(TestAgentEndpointUrl), tools: null); + + // Assert + Assert.NotNull(agent); + ChatOptions? chatOptions = GetAgentChatOptions(agent); + Assert.NotNull(chatOptions); + Assert.Null(chatOptions!.Tools); + } + #endregion #region Helper Methods diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs index 4ccd1296c5..11f36c2797 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs @@ -2,6 +2,7 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Text; @@ -356,7 +357,7 @@ public class FoundryAgentTests bool userAgentFound = false; using HttpHandlerAssert httpHandler = new(request => { - if (request.Headers.TryGetValues("User-Agent", out System.Collections.Generic.IEnumerable? values)) + if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values)) { foreach (string value in values) { @@ -676,6 +677,71 @@ public class FoundryAgentTests Assert.Equal("my-app-id", opts.UserAgentApplicationId); } + [Fact] + public void CreateProjectClientOptions_NullCallerOptions_ReturnsNull() + { + Assert.Null(FoundryAgent.CreateProjectClientOptions(null)); + } + + [Fact] + public void CreateProjectClientOptions_CarriesPipelineSettingsAndUserAgent() + { + // Arrange + var transport = new FakePipelineTransport(); + var retryPolicy = new FakeRetryPolicy(); + var messageLoggingPolicy = new FakeMessageLoggingPolicy(); + var clientLoggingOptions = new ClientLoggingOptions { EnableLogging = false }; + var networkTimeout = TimeSpan.FromSeconds(42); + + ProjectOpenAIClientOptions callerOptions = new() + { + UserAgentApplicationId = "my-app-id", + Transport = transport, + RetryPolicy = retryPolicy, + MessageLoggingPolicy = messageLoggingPolicy, + ClientLoggingOptions = clientLoggingOptions, + NetworkTimeout = networkTimeout, + }; + + // Act + AIProjectClientOptions? projectOptions = FoundryAgent.CreateProjectClientOptions(callerOptions); + + // Assert: every settable pipeline behavior the caller configured is forwarded + // onto the project-level options bag, not silently dropped. + Assert.NotNull(projectOptions); + Assert.Equal("my-app-id", projectOptions!.UserAgentApplicationId); + Assert.Same(transport, projectOptions.Transport); + Assert.Same(retryPolicy, projectOptions.RetryPolicy); + Assert.Same(messageLoggingPolicy, projectOptions.MessageLoggingPolicy); + Assert.Same(clientLoggingOptions, projectOptions.ClientLoggingOptions); + Assert.Equal(networkTimeout, projectOptions.NetworkTimeout); + } + + private sealed class FakeRetryPolicy : PipelinePolicy + { + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + => ProcessNext(message, pipeline, currentIndex); + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + => ProcessNextAsync(message, pipeline, currentIndex); + } + + private sealed class FakeMessageLoggingPolicy : PipelinePolicy + { + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + => ProcessNext(message, pipeline, currentIndex); + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + => ProcessNextAsync(message, pipeline, currentIndex); + } + + private sealed class FakePipelineTransport : PipelineTransport + { + protected override PipelineMessage CreateMessageCore() => throw new NotSupportedException(); + protected override void ProcessCore(PipelineMessage message) => throw new NotSupportedException(); + protected override ValueTask ProcessCoreAsync(PipelineMessage message) => throw new NotSupportedException(); + } + #endregion #region ParseAgentEndpoint tests @@ -758,13 +824,13 @@ public class FoundryAgentTests private readonly string _value; public HeaderStampPolicy(string name, string value) { this._name = name; this._value = value; } - public override void Process(PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { message.Request.Headers.Set(this._name, this._value); ProcessNext(message, pipeline, currentIndex); } - public override ValueTask ProcessAsync(PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { message.Request.Headers.Set(this._name, this._value); return ProcessNextAsync(message, pipeline, currentIndex);