diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs index 1a55624f3d..be66e9e635 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.AI; +using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -32,11 +34,19 @@ public static class ChatClientHarnessExtensions /// additional context providers, and chat history provider. /// When , the agent uses built-in default settings. /// + /// + /// Optional logger factory for creating loggers used by the agent and its components. + /// + /// + /// Optional service provider for resolving dependencies required by AI functions and other agent components. + /// /// A new instance. public static HarnessAgent AsHarnessAgent( this IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, - HarnessAgentOptions? options = null) => - new(chatClient, maxContextWindowTokens, maxOutputTokens, options); + HarnessAgentOptions? options = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) => + new(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services); } diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs index ef1af05513..139f47db3c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs @@ -10,6 +10,7 @@ using Microsoft.Agents.AI.Compaction; using Microsoft.Agents.AI.Tools.Shell; #endif using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -105,6 +106,12 @@ public sealed class HarnessAgent : DelegatingAIAgent /// additional context providers, and chat history provider. /// When , the agent uses built-in default settings. /// + /// + /// Optional logger factory for creating loggers used by the agent and its components. + /// + /// + /// Optional service provider for resolving dependencies required by AI functions and other agent components. + /// /// /// is . /// @@ -112,18 +119,20 @@ public sealed class HarnessAgent : DelegatingAIAgent /// is not positive, or /// is negative or greater than or equal to . /// - public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null) + public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) : base(BuildAgent( Throw.IfNull(chatClient), maxContextWindowTokens, maxOutputTokens, - options)) + options, + loggerFactory, + services)) { } - private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options) + private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services) { - ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options); + ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services); AIAgentBuilder builder = innerAgent.AsBuilder(); @@ -137,10 +146,10 @@ public sealed class HarnessAgent : DelegatingAIAgent builder.UseOpenTelemetry(sourceName: options?.OpenTelemetrySourceName); } - return builder.Build(); + return builder.Build(services); } - private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options) + private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services) { var compactionStrategy = new ContextWindowCompactionStrategy( maxContextWindowTokens: maxContextWindowTokens, @@ -165,13 +174,13 @@ public sealed class HarnessAgent : DelegatingAIAgent ChatOptions chatOptions = BuildChatOptions(options, instructions, maxOutputTokens); - var compactionProvider = new CompactionProvider(compactionStrategy); + var compactionProvider = new CompactionProvider(compactionStrategy, loggerFactory: loggerFactory); - IEnumerable contextProviders = BuildContextProviders(options); + IEnumerable contextProviders = BuildContextProviders(options, loggerFactory); return chatClient .AsBuilder() - .UseFunctionInvocation(configure: options?.MaximumIterationsPerRequest is int maxIterations + .UseFunctionInvocation(loggerFactory, configure: options?.MaximumIterationsPerRequest is int maxIterations ? ficc => ficc.MaximumIterationsPerRequest = maxIterations : null) .UseMessageInjection() @@ -189,7 +198,9 @@ public sealed class HarnessAgent : DelegatingAIAgent RequirePerServiceCallChatHistoryPersistence = true, WarnOnChatHistoryProviderConflict = false, ThrowOnChatHistoryProviderConflict = false, - }); + }, + loggerFactory, + services); } private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string instructions, int maxOutputTokens) @@ -215,7 +226,7 @@ public sealed class HarnessAgent : DelegatingAIAgent return result; } - private static List BuildContextProviders(HarnessAgentOptions? options) + private static List BuildContextProviders(HarnessAgentOptions? options, ILoggerFactory? loggerFactory) { var providers = new List(); @@ -255,8 +266,8 @@ public sealed class HarnessAgent : DelegatingAIAgent if (options?.DisableAgentSkillsProvider is not true) { AgentSkillsProvider skillsProvider = options?.AgentSkillsSource is AgentSkillsSource source - ? new AgentSkillsProvider(source) - : new AgentSkillsProvider(Directory.GetCurrentDirectory()); + ? new AgentSkillsProvider(source, loggerFactory: loggerFactory) + : new AgentSkillsProvider(Directory.GetCurrentDirectory(), loggerFactory: loggerFactory); providers.Add(skillsProvider); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index 4f08209bd8..2711fa1458 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Tools.Shell; #endif using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using Moq; namespace Microsoft.Agents.AI.UnitTests; @@ -1460,4 +1461,131 @@ public class HarnessAgentTests #endregion #endif + + #region LoggerFactory and ServiceProvider + + /// + /// Verify that the constructor succeeds when loggerFactory is provided. + /// + [Fact] + public void Constructor_SucceedsWithLoggerFactory() + { + // Arrange + var chatClient = new Mock().Object; + var loggerFactory = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that the constructor succeeds when serviceProvider is provided. + /// + [Fact] + public void Constructor_SucceedsWithServiceProvider() + { + // Arrange + var chatClient = new Mock().Object; + var services = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: services); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that the constructor succeeds when both loggerFactory and serviceProvider are provided. + /// + [Fact] + public void Constructor_SucceedsWithLoggerFactoryAndServiceProvider() + { + // Arrange + var chatClient = new Mock().Object; + var loggerFactory = new Mock().Object; + var services = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that AsHarnessAgent extension method accepts loggerFactory and serviceProvider. + /// + [Fact] + public void AsHarnessAgent_SucceedsWithLoggerFactoryAndServiceProvider() + { + // Arrange + var chatClient = new Mock().Object; + var loggerFactory = new Mock().Object; + var services = new Mock().Object; + + // Act + var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that ILoggerFactory is threaded to downstream components by confirming CreateLogger is called. + /// + [Fact] + public void Constructor_LoggerFactoryIsUsedByDownstreamComponents() + { + // Arrange + var chatClient = new Mock().Object; + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(lf => lf.CreateLogger(It.IsAny())) + .Returns(new Mock().Object); + + // Act — use options that leave CompactionProvider and AgentSkillsProvider enabled + var options = new HarnessAgentOptions + { + DisableToolApproval = true, + DisableOpenTelemetry = true, + DisableFileMemory = true, + DisableFileAccess = true, + DisableWebSearch = true, + DisableTodoProvider = true, + DisableAgentModeProvider = true, + }; + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options, mockLoggerFactory.Object); + + // Assert — CreateLogger should have been called by one or more downstream components + Assert.NotNull(agent); + mockLoggerFactory.Verify(lf => lf.CreateLogger(It.IsAny()), Times.AtLeastOnce()); + } + + /// + /// Verify that IServiceProvider is propagated through the agent pipeline by confirming + /// it is queried during agent construction. + /// + [Fact] + public void Constructor_ServiceProviderIsQueriedDuringBuild() + { + // Arrange + var chatClient = new Mock().Object; + var mockServices = new Mock(); + mockServices + .Setup(sp => sp.GetService(It.IsAny())) + .Returns(null!); + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: mockServices.Object); + + // Assert — the service provider should have been queried during pipeline construction + Assert.NotNull(agent); + mockServices.Verify(sp => sp.GetService(It.IsAny()), Times.AtLeastOnce()); + } + + #endregion }