From 2724f34723ef24b9605fc4a8f8976ff2188336f0 Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Fri, 27 Sep 2024 14:20:32 -0700 Subject: [PATCH] .Net: Adding the ability to redirect system.Console output to test output. (#9023) ### Description The PR adds the ability to redirect `system.Console` output to test output. The implementation requires a test to opt-in to this functionality so that existing tests will not change functionality by default. ***Why not just use `ITestOutputHelper ` or the `BaseTest` built in `WriteX(...)`?*** Because the process tests make use of several other classes that are writing console logs from within their functions. In order to use `ITestOutputHelper ` we would need to inject this object into those classes. While this is possible, it adds extra code that is unrelated to what the sample is demonstrating and increases the cognitive load on people using the samples to learn. fixes #9009 After this change, running Step01_Processes produces the following test output: image ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --------- Co-authored-by: Ben Thomas Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> --- .../PromptTemplates/SafeChatPrompts.cs | 18 +++++-- .../Step01_Processes.cs | 17 +++++-- .../samples/InternalUtilities/BaseTest.cs | 51 +++++++++---------- 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/dotnet/samples/Concepts/PromptTemplates/SafeChatPrompts.cs b/dotnet/samples/Concepts/PromptTemplates/SafeChatPrompts.cs index 7f81c3a0ff2e..99a07662b4b1 100644 --- a/dotnet/samples/Concepts/PromptTemplates/SafeChatPrompts.cs +++ b/dotnet/samples/Concepts/PromptTemplates/SafeChatPrompts.cs @@ -4,11 +4,12 @@ namespace PromptTemplates; -public sealed class SafeChatPrompts : BaseTest, IDisposable +public sealed class SafeChatPrompts : BaseTest { private readonly LoggingHandler _handler; private readonly HttpClient _httpClient; private readonly Kernel _kernel; + private bool _isDisposed; public SafeChatPrompts(ITestOutputHelper output) : base(output) { @@ -25,10 +26,19 @@ public SafeChatPrompts(ITestOutputHelper output) : base(output) .Build(); } - public void Dispose() + protected override void Dispose(bool disposing) { - this._handler.Dispose(); - this._httpClient.Dispose(); + if (!this._isDisposed) + { + if (disposing) + { + this._handler.Dispose(); + this._httpClient.Dispose(); + } + + this._isDisposed = true; + } + base.Dispose(disposing); } /// diff --git a/dotnet/samples/GettingStartedWithProcesses/Step01_Processes.cs b/dotnet/samples/GettingStartedWithProcesses/Step01_Processes.cs index 84ac65689d1a..ebfa7e31c87d 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step01_Processes.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step01_Processes.cs @@ -10,7 +10,7 @@ namespace GettingStartedWithProcesses; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class Step01_Processes(ITestOutputHelper output) : BaseTest(output) +public class Step01_Processes(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) { /// /// Demonstrates the creation of a simple process that has multiple steps, takes @@ -103,7 +103,9 @@ public override ValueTask ActivateAsync(KernelProcessStepState s _state = state.State; _state.UserInputs.Add("Hello"); - _state.UserInputs.Add("How are you?"); + _state.UserInputs.Add("How tall is the tallest mountain?"); + _state.UserInputs.Add("How low is the lowest valley?"); + _state.UserInputs.Add("How wide is the widest river?"); _state.UserInputs.Add("exit"); return ValueTask.CompletedTask; @@ -121,6 +123,8 @@ public async ValueTask GetUserInputAsync(KernelProcessStepContext context) var input = _state!.UserInputs[_state.CurrentInputIndex]; _state.CurrentInputIndex++; + System.Console.WriteLine($"User: {input}"); + if (input.Equals("exit", StringComparison.OrdinalIgnoreCase)) { // Emit the exit event @@ -168,11 +172,16 @@ public async Task GetChatResponseAsync(KernelProcessStepContext context, string _state!.ChatMessages.Add(new(AuthorRole.User, userMessage)); IChatCompletionService chatService = _kernel.Services.GetRequiredService(); ChatMessageContent response = await chatService.GetChatMessageContentAsync(_state.ChatMessages).ConfigureAwait(false); - if (response != null) + if (response == null) { - _state.ChatMessages.Add(response!); + throw new InvalidOperationException("Failed to get a response from the chat completion service."); } + System.Console.WriteLine($"Assistant: {response.Content}"); + + // Update state with the response + _state.ChatMessages.Add(response); + // emit event: assistantResponse await context.EmitEventAsync(new KernelProcessEvent { Id = ChatBotEvents.AssistantResponseGenerated, Data = response }); } diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 5b1916984d30..3b5c8841226f 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. using System.Reflection; +using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; -public abstract class BaseTest +public abstract class BaseTest : TextWriter { /// /// Flag to force usage of OpenAI configuration if both @@ -59,7 +60,7 @@ protected Kernel CreateKernelWithChatCompletion() return builder.Build(); } - protected BaseTest(ITestOutputHelper output) + protected BaseTest(ITestOutputHelper output, bool redirectSystemConsoleOutput = false) { this.Output = output; this.LoggerFactory = new XunitLogger(output); @@ -71,36 +72,32 @@ protected BaseTest(ITestOutputHelper output) .Build(); TestConfiguration.Initialize(configRoot); + + // Redirect System.Console output to the test output if requested + if (redirectSystemConsoleOutput) + { + System.Console.SetOut(this); + } } - /// - /// This method can be substituted by Console.WriteLine when used in Console apps. - /// - /// Target object to write - public void WriteLine(object? target = null) - => this.Output.WriteLine(target ?? string.Empty); + /// + public override void WriteLine(object? value = null) + => this.Output.WriteLine(value ?? string.Empty); - /// - /// This method can be substituted by Console.WriteLine when used in Console apps. - /// - /// Format string - /// Arguments - public void WriteLine(string? format, params object?[] args) - => this.Output.WriteLine(format ?? string.Empty, args); + /// + public override void WriteLine(string? format, params object?[] arg) + => this.Output.WriteLine(format ?? string.Empty, arg); - /// - /// This method can be substituted by Console.WriteLine when used in Console apps. - /// - /// The message - public void WriteLine(string? message) - => this.Output.WriteLine(message ?? string.Empty); + /// + public override void WriteLine(string? value) + => this.Output.WriteLine(value ?? string.Empty); - /// - /// Current interface ITestOutputHelper does not have a Write method. This extension method adds it to make it analogous to Console.Write when used in Console apps. - /// - /// Target object to write - public void Write(object? target = null) - => this.Output.WriteLine(target ?? string.Empty); + /// + public override void Write(object? value = null) + => this.Output.WriteLine(value ?? string.Empty); + + /// + public override Encoding Encoding => Encoding.UTF8; /// /// Outputs the last message in the chat history.