From 04f2c0efae6f5a2dc79818fa4f05de5649559f10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:21:27 +0000 Subject: [PATCH 1/4] Initial plan From d29514234a87036f4fa93b6009403cfb4d9f5386 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:32:25 +0000 Subject: [PATCH 2/4] feat: split console logging package Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- Extensions.AI.slnx | 1 + .../Console/ConsoleExtensions.cs | 0 .../Console/JsonConsoleLoggingExtensions.cs | 0 .../Console/JsonConsoleOptions.cs | 0 .../Console/YamlConsoleLoggingExtensions.cs | 99 ++++++++++ .../Console/YamlConsoleOptions.cs | 180 ++++++++++++++++++ .../Extensions.Console.csproj | 35 ++++ src/Extensions/Extensions.csproj | 5 +- src/Tests/ConsoleLoggingTests.cs | 43 +++++ src/Tests/Tests.csproj | 1 + 10 files changed, 361 insertions(+), 3 deletions(-) rename src/{Extensions => Extensions.Console}/Console/ConsoleExtensions.cs (100%) rename src/{Extensions => Extensions.Console}/Console/JsonConsoleLoggingExtensions.cs (100%) rename src/{Extensions => Extensions.Console}/Console/JsonConsoleOptions.cs (100%) create mode 100644 src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs create mode 100644 src/Extensions.Console/Console/YamlConsoleOptions.cs create mode 100644 src/Extensions.Console/Extensions.Console.csproj create mode 100644 src/Tests/ConsoleLoggingTests.cs diff --git a/Extensions.AI.slnx b/Extensions.AI.slnx index 9690911..a55a983 100644 --- a/Extensions.AI.slnx +++ b/Extensions.AI.slnx @@ -6,5 +6,6 @@ + diff --git a/src/Extensions/Console/ConsoleExtensions.cs b/src/Extensions.Console/Console/ConsoleExtensions.cs similarity index 100% rename from src/Extensions/Console/ConsoleExtensions.cs rename to src/Extensions.Console/Console/ConsoleExtensions.cs diff --git a/src/Extensions/Console/JsonConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs similarity index 100% rename from src/Extensions/Console/JsonConsoleLoggingExtensions.cs rename to src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs diff --git a/src/Extensions/Console/JsonConsoleOptions.cs b/src/Extensions.Console/Console/JsonConsoleOptions.cs similarity index 100% rename from src/Extensions/Console/JsonConsoleOptions.cs rename to src/Extensions.Console/Console/JsonConsoleOptions.cs diff --git a/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs new file mode 100644 index 0000000..2db290d --- /dev/null +++ b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs @@ -0,0 +1,99 @@ +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Devlooped.Extensions.AI; +using Spectre.Console; + +namespace Microsoft.Extensions.AI; + +/// +/// Adds YAML console logging capabilities to the chat client and pipeline transport. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class YamlConsoleLoggingExtensions +{ + extension(TOptions pipelineOptions) where TOptions : ClientPipelineOptions + { + /// + /// Observes the HTTP request and response messages from the underlying pipeline and renders them + /// to the console using Spectre.Console YAML formatting, but only if the console is interactive. + /// + /// The options type to configure for HTTP logging. + /// The options instance to configure. + /// + public TOptions UseYamlConsoleLogging(YamlConsoleOptions? consoleOptions = null) + { + consoleOptions ??= YamlConsoleOptions.Default; + + if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for HTTP pipeline messages?")) + return pipelineOptions; + + if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive) + return pipelineOptions; + + return pipelineOptions.Observe( + request => AnsiConsole.Write(consoleOptions.CreatePanel(request)), + response => AnsiConsole.Write(consoleOptions.CreatePanel(response))); + } + } + + extension(ChatClientBuilder builder) + { + /// + /// Renders chat messages and responses to the console using Spectre.Console YAML formatting. + /// + /// The builder in use. + /// + /// Confirmation will be asked if the console is interactive, otherwise, it will be + /// enabled unconditionally. + /// + public ChatClientBuilder UseYamlConsoleLogging(YamlConsoleOptions? consoleOptions = null) + { + consoleOptions ??= YamlConsoleOptions.Default; + + if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for HTTP pipeline messages?")) + return builder; + + if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive) + return builder; + + return builder.Use(inner => new YamlConsoleLoggingChatClient(inner, consoleOptions)); + } + } + + class YamlConsoleLoggingChatClient(IChatClient inner, YamlConsoleOptions consoleOptions) : DelegatingChatClient(inner) + { + public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + AnsiConsole.Write(consoleOptions.CreatePanel(new + { + messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), + options + })); + + var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken); + AnsiConsole.Write(consoleOptions.CreatePanel(response)); + + return response; + } + + public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + AnsiConsole.Write(consoleOptions.CreatePanel(new + { + messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), + options + })); + + List updates = []; + + await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken)) + { + updates.Add(update); + yield return update; + } + + AnsiConsole.Write(consoleOptions.CreatePanel(updates)); + } + } +} diff --git a/src/Extensions.Console/Console/YamlConsoleOptions.cs b/src/Extensions.Console/Console/YamlConsoleOptions.cs new file mode 100644 index 0000000..800b9e4 --- /dev/null +++ b/src/Extensions.Console/Console/YamlConsoleOptions.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Spectre.Console; +using Spectre.Console.Rendering; +using YamlDotNet.Serialization; + +namespace Devlooped.Extensions.AI; + +/// +/// Options for rendering YAML output to the console. +/// +public class YamlConsoleOptions +{ + static readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web) + { + DefaultIgnoreCondition = + JsonIgnoreCondition.WhenWritingNull | + JsonIgnoreCondition.WhenWritingDefault + }; + + static readonly ISerializer serializer = new SerializerBuilder().Build(); + + /// + /// Default settings for rendering YAML output to the console, which include: + /// * : true + /// * : true if console is interactive, otherwise false + /// * : true + /// + public static YamlConsoleOptions Default { get; } = new YamlConsoleOptions(); + + /// + /// Border kind for the YAML output panel. + /// + public BoxBorder Border { get; set; } = BoxBorder.Square; + + /// + /// Border style for the YAML output panel. + /// + public Style BorderStyle { get; set; } = Style.Parse("grey"); + + /// + /// Whether to include additional properties in the YAML output. + /// + /// + /// See and . + /// + public bool IncludeAdditionalProperties { get; set; } = true; + + /// + /// Confirm whether to render YAML output to the console, if the console is interactive. If + /// it is non-interactive, YAML output will be rendered conditionally based on the + /// setting. + /// + public bool InteractiveConfirm { get; set; } = ConsoleExtensions.IsConsoleInteractive; + + /// + /// Only render YAML output if the console is interactive. + /// + /// + /// This setting defaults to to avoid cluttering non-interactive console + /// outputs with YAML, while also removing the need to conditionally check for console interactivity. + /// + public bool InteractiveOnly { get; set; } = true; + + /// + /// Specifies the length at which long text will be truncated. + /// + public int? TruncateLength { get; set; } + + /// + /// Specifies the length at which long text will be wrapped automatically. + /// + public int? WrapLength { get; set; } + + internal Panel CreatePanel(object value) + { + var yaml = ToYamlString(value); + + return new Panel(WrapLength.HasValue ? new WrappedText(yaml, WrapLength.Value) : new Text(yaml, Style.Plain)) + { + Border = Border, + BorderStyle = BorderStyle, + }; + } + + internal string ToYamlString(object? value) + { + if (value is null) + return string.Empty; + + JsonNode? node = value switch + { + JsonNode existing => existing, + _ => JsonSerializer.SerializeToNode(value, value.GetType(), jsonOptions), + }; + + if (node is null) + return string.Empty; + + if (TruncateLength.HasValue || !IncludeAdditionalProperties) + node = JsonNode.Parse(node.ToShortJsonString(TruncateLength, IncludeAdditionalProperties)); + + var yaml = serializer.Serialize(ToPlain(node)); + return yaml.TrimEnd(); + } + + static object? ToPlain(JsonNode? node) + { + return node switch + { + null => null, + JsonValue value => GetValue(value), + JsonObject obj => obj.ToDictionary(kv => kv.Key, kv => ToPlain(kv.Value)), + JsonArray arr => arr.Select(ToPlain).ToArray(), + _ => node.ToJsonString() + }; + } + + static object? GetValue(JsonValue value) + { + if (value.TryGetValue(out bool boolean)) + return boolean; + if (value.TryGetValue(out int number)) + return number; + if (value.TryGetValue(out long longNumber)) + return longNumber; + if (value.TryGetValue(out double real)) + return real; + if (value.TryGetValue(out string? str)) + return str; + + return value.GetValue(); + } + +#pragma warning disable CS9113 // Parameter is unread. BOGUS + sealed class WrappedText(string text, int maxWidth) : Renderable +#pragma warning restore CS9113 // Parameter is unread. BOGUS + { + readonly Text plainText = new(text, Style.Plain); + + protected override Measurement Measure(RenderOptions options, int maxWidth) + { + return new Measurement(Math.Min(maxWidth, this.maxWidth), Math.Min(maxWidth, this.maxWidth)); + } + + protected override IEnumerable Render(RenderOptions options, int maxWidth) + { + var segments = ((IRenderable)plainText).Render(options, maxWidth).ToList(); + var wrapped = new List(); + + foreach (var segment in segments) + { + if (segment.IsLineBreak) + { + wrapped.Add(Segment.LineBreak); + continue; + } + + var value = segment.Text; + var style = segment.Style ?? Style.Plain; + + var idx = 0; + while (idx < value.Length) + { + var len = Math.Min(this.maxWidth, value.Length - idx); + wrapped.Add(new Segment(value.Substring(idx, len), style)); + idx += len; + if (idx < value.Length) + wrapped.Add(Segment.LineBreak); + } + } + + return wrapped; + } + } +} diff --git a/src/Extensions.Console/Extensions.Console.csproj b/src/Extensions.Console/Extensions.Console.csproj new file mode 100644 index 0000000..a33b15c --- /dev/null +++ b/src/Extensions.Console/Extensions.Console.csproj @@ -0,0 +1,35 @@ + + + + net8.0;net10.0 + Preview + Devlooped.Extensions.AI.Console + Devlooped.Extensions.AI + Devlooped.Extensions.AI.Console + Console logging extensions for Devlooped.Extensions.AI + + OSMFEULA.txt + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions/Extensions.csproj b/src/Extensions/Extensions.csproj index a82d07d..70c9166 100644 --- a/src/Extensions/Extensions.csproj +++ b/src/Extensions/Extensions.csproj @@ -24,8 +24,6 @@ - - @@ -40,8 +38,9 @@ + - \ No newline at end of file + diff --git a/src/Tests/ConsoleLoggingTests.cs b/src/Tests/ConsoleLoggingTests.cs new file mode 100644 index 0000000..92a44e7 --- /dev/null +++ b/src/Tests/ConsoleLoggingTests.cs @@ -0,0 +1,43 @@ +using Devlooped.Extensions.AI; +using Spectre.Console; + +namespace Devlooped; + +public class ConsoleLoggingTests +{ + [Fact] + public void YamlOptionsExcludeAdditionalProperties() + { + var options = new YamlConsoleOptions + { + IncludeAdditionalProperties = false, + TruncateLength = 5 + }; + + var yaml = options.ToYamlString(new + { + Message = "123456789", + AdditionalProperties = new { Ignored = "value" } + }); + + Assert.DoesNotContain("AdditionalProperties", yaml); + Assert.Contains("12345...", yaml); + Assert.Contains("Message", yaml); + } + + [Fact] + public void YamlPanelRespectsCustomization() + { + var options = new YamlConsoleOptions + { + Border = BoxBorder.Ascii, + BorderStyle = Style.Parse("red"), + WrapLength = 10 + }; + + var panel = options.CreatePanel(new { Message = "hello" }); + + Assert.Equal(BoxBorder.Ascii, panel.Border); + Assert.Equal(Style.Parse("red"), panel.BorderStyle); + } +} diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 9e22f31..b6397ec 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -33,6 +33,7 @@ + From abae8855e2887622e42d5f9d10ab0accaa53a903 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:34:46 +0000 Subject: [PATCH 3/4] chore: address console review feedback Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- .../Console/JsonConsoleLoggingExtensions.cs | 2 +- src/Extensions.Console/Console/JsonConsoleOptions.cs | 5 +++-- .../Console/YamlConsoleLoggingExtensions.cs | 2 +- src/Extensions.Console/Console/YamlConsoleOptions.cs | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs index 63797dc..bd024b9 100644 --- a/src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs +++ b/src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs @@ -51,7 +51,7 @@ public ChatClientBuilder UseJsonConsoleLogging(JsonConsoleOptions? consoleOption { consoleOptions ??= JsonConsoleOptions.Default; - if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?")) + if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for chat messages and responses?")) return builder; if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive) diff --git a/src/Extensions.Console/Console/JsonConsoleOptions.cs b/src/Extensions.Console/Console/JsonConsoleOptions.cs index c5fe1c8..45cb341 100644 --- a/src/Extensions.Console/Console/JsonConsoleOptions.cs +++ b/src/Extensions.Console/Console/JsonConsoleOptions.cs @@ -152,7 +152,8 @@ sealed class WrappedJsonText(string json, int maxWidth) : Renderable protected override Measurement Measure(RenderOptions options, int maxWidth) { // Clamp the measurement to the desired maxWidth - return new Measurement(Math.Min(maxWidth, maxWidth), Math.Min(maxWidth, maxWidth)); + var width = Math.Min(this.maxWidth, maxWidth); + return new Measurement(width, width); } protected override IEnumerable Render(RenderOptions options, int maxWidth) @@ -170,7 +171,7 @@ protected override IEnumerable Render(RenderOptions options, int maxWid var idx = 0; while (idx < text.Length) { - var len = Math.Min(maxWidth, text.Length - idx); + var len = Math.Min(this.maxWidth, text.Length - idx); wrapped.Add(new Segment(text.Substring(idx, len), style)); idx += len; if (idx < text.Length) diff --git a/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs index 2db290d..c2aa8dc 100644 --- a/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs +++ b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs @@ -51,7 +51,7 @@ public ChatClientBuilder UseYamlConsoleLogging(YamlConsoleOptions? consoleOption { consoleOptions ??= YamlConsoleOptions.Default; - if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for HTTP pipeline messages?")) + if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for chat messages and responses?")) return builder; if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive) diff --git a/src/Extensions.Console/Console/YamlConsoleOptions.cs b/src/Extensions.Console/Console/YamlConsoleOptions.cs index 800b9e4..07892bb 100644 --- a/src/Extensions.Console/Console/YamlConsoleOptions.cs +++ b/src/Extensions.Console/Console/YamlConsoleOptions.cs @@ -15,7 +15,7 @@ namespace Devlooped.Extensions.AI; /// public class YamlConsoleOptions { - static readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web) + static readonly JsonSerializerOptions serializationOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | @@ -95,7 +95,7 @@ internal string ToYamlString(object? value) JsonNode? node = value switch { JsonNode existing => existing, - _ => JsonSerializer.SerializeToNode(value, value.GetType(), jsonOptions), + _ => JsonSerializer.SerializeToNode(value, value.GetType(), serializationOptions), }; if (node is null) From ae3701f05d3a1256a45e99298a840c65c1574baa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:34:25 +0000 Subject: [PATCH 4/4] chore: remove yaml console logging Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- .../Console/YamlConsoleLoggingExtensions.cs | 99 ---------- .../Console/YamlConsoleOptions.cs | 180 ------------------ .../Extensions.Console.csproj | 1 - src/Tests/ConsoleLoggingTests.cs | 43 ----- 4 files changed, 323 deletions(-) delete mode 100644 src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs delete mode 100644 src/Extensions.Console/Console/YamlConsoleOptions.cs delete mode 100644 src/Tests/ConsoleLoggingTests.cs diff --git a/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs deleted file mode 100644 index c2aa8dc..0000000 --- a/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.ClientModel.Primitives; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Devlooped.Extensions.AI; -using Spectre.Console; - -namespace Microsoft.Extensions.AI; - -/// -/// Adds YAML console logging capabilities to the chat client and pipeline transport. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public static class YamlConsoleLoggingExtensions -{ - extension(TOptions pipelineOptions) where TOptions : ClientPipelineOptions - { - /// - /// Observes the HTTP request and response messages from the underlying pipeline and renders them - /// to the console using Spectre.Console YAML formatting, but only if the console is interactive. - /// - /// The options type to configure for HTTP logging. - /// The options instance to configure. - /// - public TOptions UseYamlConsoleLogging(YamlConsoleOptions? consoleOptions = null) - { - consoleOptions ??= YamlConsoleOptions.Default; - - if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for HTTP pipeline messages?")) - return pipelineOptions; - - if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive) - return pipelineOptions; - - return pipelineOptions.Observe( - request => AnsiConsole.Write(consoleOptions.CreatePanel(request)), - response => AnsiConsole.Write(consoleOptions.CreatePanel(response))); - } - } - - extension(ChatClientBuilder builder) - { - /// - /// Renders chat messages and responses to the console using Spectre.Console YAML formatting. - /// - /// The builder in use. - /// - /// Confirmation will be asked if the console is interactive, otherwise, it will be - /// enabled unconditionally. - /// - public ChatClientBuilder UseYamlConsoleLogging(YamlConsoleOptions? consoleOptions = null) - { - consoleOptions ??= YamlConsoleOptions.Default; - - if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for chat messages and responses?")) - return builder; - - if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive) - return builder; - - return builder.Use(inner => new YamlConsoleLoggingChatClient(inner, consoleOptions)); - } - } - - class YamlConsoleLoggingChatClient(IChatClient inner, YamlConsoleOptions consoleOptions) : DelegatingChatClient(inner) - { - public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - AnsiConsole.Write(consoleOptions.CreatePanel(new - { - messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), - options - })); - - var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken); - AnsiConsole.Write(consoleOptions.CreatePanel(response)); - - return response; - } - - public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - AnsiConsole.Write(consoleOptions.CreatePanel(new - { - messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), - options - })); - - List updates = []; - - await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken)) - { - updates.Add(update); - yield return update; - } - - AnsiConsole.Write(consoleOptions.CreatePanel(updates)); - } - } -} diff --git a/src/Extensions.Console/Console/YamlConsoleOptions.cs b/src/Extensions.Console/Console/YamlConsoleOptions.cs deleted file mode 100644 index 07892bb..0000000 --- a/src/Extensions.Console/Console/YamlConsoleOptions.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; -using Spectre.Console; -using Spectre.Console.Rendering; -using YamlDotNet.Serialization; - -namespace Devlooped.Extensions.AI; - -/// -/// Options for rendering YAML output to the console. -/// -public class YamlConsoleOptions -{ - static readonly JsonSerializerOptions serializationOptions = new(JsonSerializerDefaults.Web) - { - DefaultIgnoreCondition = - JsonIgnoreCondition.WhenWritingNull | - JsonIgnoreCondition.WhenWritingDefault - }; - - static readonly ISerializer serializer = new SerializerBuilder().Build(); - - /// - /// Default settings for rendering YAML output to the console, which include: - /// * : true - /// * : true if console is interactive, otherwise false - /// * : true - /// - public static YamlConsoleOptions Default { get; } = new YamlConsoleOptions(); - - /// - /// Border kind for the YAML output panel. - /// - public BoxBorder Border { get; set; } = BoxBorder.Square; - - /// - /// Border style for the YAML output panel. - /// - public Style BorderStyle { get; set; } = Style.Parse("grey"); - - /// - /// Whether to include additional properties in the YAML output. - /// - /// - /// See and . - /// - public bool IncludeAdditionalProperties { get; set; } = true; - - /// - /// Confirm whether to render YAML output to the console, if the console is interactive. If - /// it is non-interactive, YAML output will be rendered conditionally based on the - /// setting. - /// - public bool InteractiveConfirm { get; set; } = ConsoleExtensions.IsConsoleInteractive; - - /// - /// Only render YAML output if the console is interactive. - /// - /// - /// This setting defaults to to avoid cluttering non-interactive console - /// outputs with YAML, while also removing the need to conditionally check for console interactivity. - /// - public bool InteractiveOnly { get; set; } = true; - - /// - /// Specifies the length at which long text will be truncated. - /// - public int? TruncateLength { get; set; } - - /// - /// Specifies the length at which long text will be wrapped automatically. - /// - public int? WrapLength { get; set; } - - internal Panel CreatePanel(object value) - { - var yaml = ToYamlString(value); - - return new Panel(WrapLength.HasValue ? new WrappedText(yaml, WrapLength.Value) : new Text(yaml, Style.Plain)) - { - Border = Border, - BorderStyle = BorderStyle, - }; - } - - internal string ToYamlString(object? value) - { - if (value is null) - return string.Empty; - - JsonNode? node = value switch - { - JsonNode existing => existing, - _ => JsonSerializer.SerializeToNode(value, value.GetType(), serializationOptions), - }; - - if (node is null) - return string.Empty; - - if (TruncateLength.HasValue || !IncludeAdditionalProperties) - node = JsonNode.Parse(node.ToShortJsonString(TruncateLength, IncludeAdditionalProperties)); - - var yaml = serializer.Serialize(ToPlain(node)); - return yaml.TrimEnd(); - } - - static object? ToPlain(JsonNode? node) - { - return node switch - { - null => null, - JsonValue value => GetValue(value), - JsonObject obj => obj.ToDictionary(kv => kv.Key, kv => ToPlain(kv.Value)), - JsonArray arr => arr.Select(ToPlain).ToArray(), - _ => node.ToJsonString() - }; - } - - static object? GetValue(JsonValue value) - { - if (value.TryGetValue(out bool boolean)) - return boolean; - if (value.TryGetValue(out int number)) - return number; - if (value.TryGetValue(out long longNumber)) - return longNumber; - if (value.TryGetValue(out double real)) - return real; - if (value.TryGetValue(out string? str)) - return str; - - return value.GetValue(); - } - -#pragma warning disable CS9113 // Parameter is unread. BOGUS - sealed class WrappedText(string text, int maxWidth) : Renderable -#pragma warning restore CS9113 // Parameter is unread. BOGUS - { - readonly Text plainText = new(text, Style.Plain); - - protected override Measurement Measure(RenderOptions options, int maxWidth) - { - return new Measurement(Math.Min(maxWidth, this.maxWidth), Math.Min(maxWidth, this.maxWidth)); - } - - protected override IEnumerable Render(RenderOptions options, int maxWidth) - { - var segments = ((IRenderable)plainText).Render(options, maxWidth).ToList(); - var wrapped = new List(); - - foreach (var segment in segments) - { - if (segment.IsLineBreak) - { - wrapped.Add(Segment.LineBreak); - continue; - } - - var value = segment.Text; - var style = segment.Style ?? Style.Plain; - - var idx = 0; - while (idx < value.Length) - { - var len = Math.Min(this.maxWidth, value.Length - idx); - wrapped.Add(new Segment(value.Substring(idx, len), style)); - idx += len; - if (idx < value.Length) - wrapped.Add(Segment.LineBreak); - } - } - - return wrapped; - } - } -} diff --git a/src/Extensions.Console/Extensions.Console.csproj b/src/Extensions.Console/Extensions.Console.csproj index a33b15c..e767bb2 100644 --- a/src/Extensions.Console/Extensions.Console.csproj +++ b/src/Extensions.Console/Extensions.Console.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Tests/ConsoleLoggingTests.cs b/src/Tests/ConsoleLoggingTests.cs deleted file mode 100644 index 92a44e7..0000000 --- a/src/Tests/ConsoleLoggingTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Devlooped.Extensions.AI; -using Spectre.Console; - -namespace Devlooped; - -public class ConsoleLoggingTests -{ - [Fact] - public void YamlOptionsExcludeAdditionalProperties() - { - var options = new YamlConsoleOptions - { - IncludeAdditionalProperties = false, - TruncateLength = 5 - }; - - var yaml = options.ToYamlString(new - { - Message = "123456789", - AdditionalProperties = new { Ignored = "value" } - }); - - Assert.DoesNotContain("AdditionalProperties", yaml); - Assert.Contains("12345...", yaml); - Assert.Contains("Message", yaml); - } - - [Fact] - public void YamlPanelRespectsCustomization() - { - var options = new YamlConsoleOptions - { - Border = BoxBorder.Ascii, - BorderStyle = Style.Parse("red"), - WrapLength = 10 - }; - - var panel = options.CreatePanel(new { Message = "hello" }); - - Assert.Equal(BoxBorder.Ascii, panel.Border); - Assert.Equal(Style.Parse("red"), panel.BorderStyle); - } -}