From a7f1f795e2d359e53d520bbad32c35f56d7d6e26 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 30 Jun 2025 18:07:49 -0300 Subject: [PATCH] Add JSON console output rendering --- .netconfig | 10 + src/AI.Tests/AI.Tests.csproj | 2 +- src/AI/AI.csproj | 7 +- src/AI/Console/ConsoleExtensions.cs | 10 + .../Console/JsonConsoleLoggingChatClient.cs | 57 + .../Console/JsonConsoleLoggingExtensions.cs | 119 +++ src/AI/Extensions/System/Throw.cs | 992 ++++++++++++++++++ src/AI/JsonConsoleLoggingExtensions.cs | 63 -- src/AI/JsonExtensions.cs | 11 +- 9 files changed, 1201 insertions(+), 70 deletions(-) create mode 100644 src/AI/Console/ConsoleExtensions.cs create mode 100644 src/AI/Console/JsonConsoleLoggingChatClient.cs create mode 100644 src/AI/Console/JsonConsoleLoggingExtensions.cs create mode 100644 src/AI/Extensions/System/Throw.cs delete mode 100644 src/AI/JsonConsoleLoggingExtensions.cs diff --git a/.netconfig b/.netconfig index 9015907..c387778 100644 --- a/.netconfig +++ b/.netconfig @@ -151,3 +151,13 @@ sha = 77e83f238196d2723640abef0c7b6f43994f9747 etag = fcb9759a96966df40dcd24906fd328ddec05953b7e747a6bb8d0d1e4c3865274 weak +[file "src/AI/Extensions/System/Throw.cs"] + url = https://github.com/devlooped/catbag/blob/main/System/Throw.cs + sha = 3012d56be7554c483e5c5d277144c063969cada9 + etag = 43c81c6c6dcdf5baee40a9e3edc5e871e473e6c954c901b82bb87a3a48888ea0 + weak +[file "src/Weaving/Extensions/System/Throw.cs"] + url = https://github.com/devlooped/catbag/blob/main/System/Throw.cs + sha = 3012d56be7554c483e5c5d277144c063969cada9 + etag = 43c81c6c6dcdf5baee40a9e3edc5e871e473e6c954c901b82bb87a3a48888ea0 + weak diff --git a/src/AI.Tests/AI.Tests.csproj b/src/AI.Tests/AI.Tests.csproj index f63736c..198de5d 100644 --- a/src/AI.Tests/AI.Tests.csproj +++ b/src/AI.Tests/AI.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/AI/AI.csproj b/src/AI/AI.csproj index 5f9ffa0..e06d105 100644 --- a/src/AI/AI.csproj +++ b/src/AI/AI.csproj @@ -2,14 +2,17 @@ net8.0 + Preview Devlooped.Extensions.AI Extensions for Microsoft.Extensions.AI - true + - + + + diff --git a/src/AI/Console/ConsoleExtensions.cs b/src/AI/Console/ConsoleExtensions.cs new file mode 100644 index 0000000..2a9b90e --- /dev/null +++ b/src/AI/Console/ConsoleExtensions.cs @@ -0,0 +1,10 @@ +namespace Devlooped.Extensions.AI; + +static class ConsoleExtensions +{ + public static bool IsConsoleInteractive => + !Console.IsInputRedirected && + !Console.IsOutputRedirected && + !Console.IsErrorRedirected && + Environment.UserInteractive; +} diff --git a/src/AI/Console/JsonConsoleLoggingChatClient.cs b/src/AI/Console/JsonConsoleLoggingChatClient.cs new file mode 100644 index 0000000..7f83a1f --- /dev/null +++ b/src/AI/Console/JsonConsoleLoggingChatClient.cs @@ -0,0 +1,57 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.AI; +using Spectre.Console; +using Spectre.Console.Json; + +namespace Devlooped.Extensions.AI; + +/// +/// Chat client that logs messages and responses to the console in JSON format using Spectre.Console. +/// +/// +public class JsonConsoleLoggingChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient) +{ + /// + /// Whether to include additional properties in the JSON output. + /// + public bool IncludeAdditionalProperties { get; set; } = true; + + /// + /// Optional maximum length to render for string values. Replaces remaining characters with "...". + /// + public int? MaxLength { get; set; } + + public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + AnsiConsole.Write(new Panel(new JsonText(new + { + messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), + options + }.ToJsonString(MaxLength, IncludeAdditionalProperties)))); + + var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken); + + AnsiConsole.Write(new Panel(new JsonText(response.ToJsonString(MaxLength, IncludeAdditionalProperties)))); + return response; + } + + public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + AnsiConsole.Write(new Panel(new JsonText(new + { + messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), + options + }.ToJsonString(MaxLength, IncludeAdditionalProperties)))); + + List updates = []; + + await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken)) + { + updates.Add(update); + yield return update; + } + + AnsiConsole.Write(new Panel(new JsonText(updates.ToJsonString(MaxLength, IncludeAdditionalProperties)))); + } +} + diff --git a/src/AI/Console/JsonConsoleLoggingExtensions.cs b/src/AI/Console/JsonConsoleLoggingExtensions.cs new file mode 100644 index 0000000..88d1e41 --- /dev/null +++ b/src/AI/Console/JsonConsoleLoggingExtensions.cs @@ -0,0 +1,119 @@ +using System.ClientModel.Primitives; +using System.ComponentModel; +using Devlooped.Extensions.AI; +using Spectre.Console; +using Spectre.Console.Json; + +namespace Microsoft.Extensions.AI; + +/// +/// Adds console logging capabilities to the chat client and pipeline transport. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class JsonConsoleLoggingExtensions +{ + /// + /// Sets a that renders HTTP messages to the + /// console using Spectre.Console rich JSON formatting, but only if the console is interactive. + /// + /// The options type to configure for HTTP logging. + /// The options instance to configure. + /// + /// NOTE: this is the lowst-level logging after all chat pipeline processing has been done. + /// + /// If the options already provide a transport, it will be wrapped with the console + /// logging transport to minimize the impact on existing configurations. + /// + /// + public static TOptions UseJsonConsoleLogging(this TOptions options) + where TOptions : ClientPipelineOptions + => UseJsonConsoleLogging(options, ConsoleExtensions.IsConsoleInteractive); + + /// + /// Sets a that renders HTTP messages to the + /// console using Spectre.Console rich JSON formatting. + /// + /// The options type to configure for HTTP logging. + /// The options instance to configure. + /// Whether to confirm logging before enabling it. + /// + /// NOTE: this is the lowst-level logging after all chat pipeline processing has been done. + /// + /// If the options already provide a transport, it will be wrapped with the console + /// logging transport to minimize the impact on existing configurations. + /// + /// + public static TOptions UseJsonConsoleLogging(this TOptions options, bool askConfirmation) + where TOptions : ClientPipelineOptions + { + if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?")) + return options; + + options.Transport = new ConsoleLoggingPipelineTransport(options.Transport ?? HttpClientPipelineTransport.Shared); + + return options; + } + + /// + /// Renders chat messages and responses to the console using Spectre.Console rich JSON formatting. + /// + /// The builder in use. + /// + /// Confirmation will be asked if the console is interactive, otherwise, it will be + /// enabled unconditionally. + /// + public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder) + => UseJsonConsoleLogging(builder, ConsoleExtensions.IsConsoleInteractive); + + /// + /// Renders chat messages and responses to the console using Spectre.Console rich JSON formatting. + /// + /// The builder in use. + /// If true, prompts the user for confirmation before enabling console logging. + /// Optional maximum length to render for string values. Replaces remaining characters with "...". + public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder, bool askConfirmation = false, Action? configure = null) + { + if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable console logging for chat messages?")) + return builder; + + return builder.Use(inner => + { + var client = new JsonConsoleLoggingChatClient(inner); + configure?.Invoke(client); + return client; + }); + } + + class ConsoleLoggingPipelineTransport(PipelineTransport inner) : PipelineTransport + { + public static PipelineTransport Default { get; } = new ConsoleLoggingPipelineTransport(); + + public ConsoleLoggingPipelineTransport() : this(HttpClientPipelineTransport.Shared) { } + + protected override PipelineMessage CreateMessageCore() => inner.CreateMessage(); + protected override void ProcessCore(PipelineMessage message) => inner.Process(message); + + protected override async ValueTask ProcessCoreAsync(PipelineMessage message) + { + message.BufferResponse = true; + await inner.ProcessAsync(message); + + if (message.Request.Content is not null) + { + using var memory = new MemoryStream(); + message.Request.Content.WriteTo(memory); + memory.Position = 0; + using var reader = new StreamReader(memory); + var content = await reader.ReadToEndAsync(); + + AnsiConsole.Write(new Panel(new JsonText(content))); + } + + if (message.Response != null) + { + AnsiConsole.Write(new Panel(new JsonText(message.Response.Content.ToString()))); + } + } + } + +} diff --git a/src/AI/Extensions/System/Throw.cs b/src/AI/Extensions/System/Throw.cs new file mode 100644 index 0000000..eea3e12 --- /dev/null +++ b/src/AI/Extensions/System/Throw.cs @@ -0,0 +1,992 @@ +// +#region License +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// Adapted from https://github.com/dotnet/extensions/blob/main/src/Shared/Throw/Throw.cs +#endregion + +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#pragma warning disable CA1716 +namespace System; +#pragma warning restore CA1716 + +/// +/// Defines static methods used to throw exceptions. +/// +/// +/// The main purpose is to reduce code size, improve performance, and standardize exception +/// messages. +/// +[SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "Doesn't work with the region layout")] +[SuppressMessage("Minor Code Smell", "S2333:Partial is gratuitous in this context", Justification = "Some projects add additional partial parts.")] +[SuppressMessage("Design", "CA1716", Justification = "Not part of an API")] + +#if !SHARED_PROJECT +[ExcludeFromCodeCoverage] +#endif + +static partial class Throw +{ + #region For Object + + /// + /// Throws an if the specified argument is . + /// + /// Argument type to be checked for . + /// Object to be checked for . + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static T IfNull([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument is null) + { + ArgumentNullException(paramName); + } + + return argument; + } + + /// + /// Throws an if the specified argument is , + /// or if the specified member is . + /// + /// Argument type to be checked for . + /// Member type to be checked for . + /// Argument to be checked for . + /// Object member to be checked for . + /// The name of the parameter being checked. + /// The name of the member. + /// The original value of . + /// + /// + /// Throws.IfNullOrMemberNull(myObject, myObject?.MyProperty) + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static TMember IfNullOrMemberNull( + [NotNull] TParameter argument, + [NotNull] TMember member, + [CallerArgumentExpression(nameof(argument))] string paramName = "", + [CallerArgumentExpression(nameof(member))] string memberName = "") + { + if (argument is null) + { + ArgumentNullException(paramName); + } + + if (member is null) + { + ArgumentException(paramName, $"Member {memberName} of {paramName} is null"); + } + + return member; + } + + /// + /// Throws an if the specified member is . + /// + /// Argument type. + /// Member type to be checked for . + /// Argument to which member belongs. + /// Object member to be checked for . + /// The name of the parameter being checked. + /// The name of the member. + /// The original value of . + /// + /// + /// Throws.IfMemberNull(myObject, myObject.MyProperty) + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Analyzer isn't seeing the reference to 'argument' in the attribute")] + public static TMember IfMemberNull( + TParameter argument, + [NotNull] TMember member, + [CallerArgumentExpression(nameof(argument))] string paramName = "", + [CallerArgumentExpression(nameof(member))] string memberName = "") + where TParameter : notnull + { + if (member is null) + { + ArgumentException(paramName, $"Member {memberName} of {paramName} is null"); + } + + return member; + } + + #endregion + + #region For String + + /// + /// Throws either an or an + /// if the specified string is or whitespace respectively. + /// + /// String to be checked for or whitespace. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static string IfNullOrWhitespace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { +#if !NETCOREAPP3_1_OR_GREATER + if (argument == null) + { + ArgumentNullException(paramName); + } +#endif + + if (string.IsNullOrWhiteSpace(argument)) + { + if (argument == null) + { + ArgumentNullException(paramName); + } + else + { + ArgumentException(paramName, "Argument is whitespace"); + } + } + + return argument; + } + + /// + /// Throws an if the string is , + /// or if it is empty. + /// + /// String to be checked for or empty. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static string IfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { +#if !NETCOREAPP3_1_OR_GREATER + if (argument == null) + { + ArgumentNullException(paramName); + } +#endif + + if (string.IsNullOrEmpty(argument)) + { + if (argument == null) + { + ArgumentNullException(paramName); + } + else + { + ArgumentException(paramName, "Argument is an empty string"); + } + } + + return argument; + } + + #endregion + + #region For Buffer + + /// + /// Throws an if the argument's buffer size is less than the required buffer size. + /// + /// The actual buffer size. + /// The required buffer size. + /// The name of the parameter to be checked. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IfBufferTooSmall(int bufferSize, int requiredSize, string paramName = "") + { + if (bufferSize < requiredSize) + { + ArgumentException(paramName, $"Buffer too small, needed a size of {requiredSize} but got {bufferSize}"); + } + } + + #endregion + + #region For Enums + + /// + /// Throws an if the enum value is not valid. + /// + /// The argument to evaluate. + /// The name of the parameter being checked. + /// The type of the enumeration. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T IfOutOfRange(T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + where T : struct, Enum + { +#if NET5_0_OR_GREATER + if (!Enum.IsDefined(argument)) +#else + if (!Enum.IsDefined(typeof(T), argument)) +#endif + { + ArgumentOutOfRangeException(paramName, $"{argument} is an invalid value for enum type {typeof(T)}"); + } + + return argument; + } + + #endregion + + #region For Collections + + /// + /// Throws an if the collection is , + /// or if it is empty. + /// + /// The collection to evaluate. + /// The name of the parameter being checked. + /// The type of objects in the collection. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + + // The method has actually 100% coverage, but due to a bug in the code coverage tool, + // a lower number is reported. Therefore, we temporarily exclude this method + // from the coverage measurements. Once the bug in the code coverage tool is fixed, + // the exclusion attribute can be removed. + [ExcludeFromCodeCoverage] + public static IEnumerable IfNullOrEmpty([NotNull] IEnumerable? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == null) + { + ArgumentNullException(paramName); + } + else + { + switch (argument) + { + case ICollection collection: + if (collection.Count == 0) + { + ArgumentException(paramName, "Collection is empty"); + } + + break; + case IReadOnlyCollection readOnlyCollection: + if (readOnlyCollection.Count == 0) + { + ArgumentException(paramName, "Collection is empty"); + } + + break; + default: + using (IEnumerator enumerator = argument.GetEnumerator()) + { + if (!enumerator.MoveNext()) + { + ArgumentException(paramName, "Collection is empty"); + } + } + + break; + } + } + + return argument; + } + + #endregion + + #region Exceptions + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentNullException(string paramName) + => throw new ArgumentNullException(paramName); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentNullException(string paramName, string? message) + => throw new ArgumentNullException(paramName, message); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName) + => throw new ArgumentOutOfRangeException(paramName); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName, string? message) + => throw new ArgumentOutOfRangeException(paramName, message); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// The value of the argument that caused this exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName, object? actualValue, string? message) + => throw new ArgumentOutOfRangeException(paramName, actualValue, message); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentException(string paramName, string? message) + => throw new ArgumentException(message, paramName); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. + /// The exception that is the cause of the current exception. + /// + /// If the is not a , the current exception is raised in a catch + /// block that handles the inner exception. + /// +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentException(string paramName, string? message, Exception? innerException) + => throw new ArgumentException(message, paramName, innerException); + + /// + /// Throws an . + /// + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void InvalidOperationException(string message) + => throw new InvalidOperationException(message); + + /// + /// Throws an . + /// + /// A message that describes the error. + /// The exception that is the cause of the current exception. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void InvalidOperationException(string message, Exception? innerException) + => throw new InvalidOperationException(message, innerException); + + #endregion + + #region For Integer + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfLessThan(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfGreaterThan(int argument, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfLessThanOrEqual(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfGreaterThanOrEqual(int argument, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfOutOfRange(int argument, int min, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfZero(int argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Unsigned Integer + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfLessThan(uint argument, uint min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfGreaterThan(uint argument, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfLessThanOrEqual(uint argument, uint min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfGreaterThanOrEqual(uint argument, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfOutOfRange(uint argument, uint min, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfZero(uint argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0U) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Long + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfLessThan(long argument, long min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfGreaterThan(long argument, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfLessThanOrEqual(long argument, long min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfGreaterThanOrEqual(long argument, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfOutOfRange(long argument, long min, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfZero(long argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0L) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Unsigned Long + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfLessThan(ulong argument, ulong min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfGreaterThan(ulong argument, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfLessThanOrEqual(ulong argument, ulong min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfGreaterThanOrEqual(ulong argument, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfOutOfRange(ulong argument, ulong min, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfZero(ulong argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0UL) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Double + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfLessThan(double argument, double min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + // strange conditional needed in order to handle NaN values correctly +#pragma warning disable S1940 // Boolean checks should not be inverted + if (!(argument >= min)) +#pragma warning restore S1940 // Boolean checks should not be inverted + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfGreaterThan(double argument, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + // strange conditional needed in order to handle NaN values correctly +#pragma warning disable S1940 // Boolean checks should not be inverted + if (!(argument <= max)) +#pragma warning restore S1940 // Boolean checks should not be inverted + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfLessThanOrEqual(double argument, double min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + // strange conditional needed in order to handle NaN values correctly +#pragma warning disable S1940 // Boolean checks should not be inverted + if (!(argument > min)) +#pragma warning restore S1940 // Boolean checks should not be inverted + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfGreaterThanOrEqual(double argument, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + // strange conditional needed in order to handle NaN values correctly +#pragma warning disable S1940 // Boolean checks should not be inverted + if (!(argument < max)) +#pragma warning restore S1940 // Boolean checks should not be inverted + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfOutOfRange(double argument, double min, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + // strange conditional needed in order to handle NaN values correctly + if (!(min <= argument && argument <= max)) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfZero(double argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { +#pragma warning disable S1244 // Floating point numbers should not be tested for equality + if (argument == 0.0) +#pragma warning restore S1244 // Floating point numbers should not be tested for equality + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion +} diff --git a/src/AI/JsonConsoleLoggingExtensions.cs b/src/AI/JsonConsoleLoggingExtensions.cs deleted file mode 100644 index a062521..0000000 --- a/src/AI/JsonConsoleLoggingExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Devlooped.Extensions.AI; -using Spectre.Console; -using Spectre.Console.Json; - -namespace Microsoft.Extensions.AI; - -/// -/// Adds console logging capabilities to the chat client. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public static class JsonConsoleLoggingExtensions -{ - /// - /// Renders chat messages and responses to the console using Spectre.Console rich JSON formatting. - /// - /// The builder in use. - /// If true, prompts the user for confirmation before enabling console logging. - public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder, bool askConfirmation = false) - { - if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable console logging for chat messages?")) - return builder; - - return builder.Use(inner => new ConsoleLoggingChatClient(inner)); - } - - class ConsoleLoggingChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient) - { - public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - AnsiConsole.Write(new Panel(new JsonText(new - { - messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), - options - }.ToJsonString()))); - - var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken); - - AnsiConsole.Write(new Panel(new JsonText(response.ToJsonString()))); - return response; - } - - public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - AnsiConsole.Write(new Panel(new JsonText(new - { - messages = messages.Where(x => x.Role != ChatRole.System).ToArray(), - options - }.ToJsonString()))); - - List updates = []; - - await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken)) - { - updates.Add(update); - yield return update; - } - - AnsiConsole.Write(new Panel(new JsonText(updates.ToJsonString()))); - } - } -} diff --git a/src/AI/JsonExtensions.cs b/src/AI/JsonExtensions.cs index 7b94e3a..b652d2e 100644 --- a/src/AI/JsonExtensions.cs +++ b/src/AI/JsonExtensions.cs @@ -10,7 +10,7 @@ static class JsonExtensions /// /// Recursively truncates long strings in an object before serialization and optionally excludes additional properties. /// - public static string ToJsonString(this object? value, int maxStringLength = 100, bool includeAdditionalProperties = true) + public static string ToJsonString(this object? value, int? maxStringLength = 100, bool includeAdditionalProperties = true) { if (value is null) return "{}"; @@ -19,7 +19,7 @@ public static string ToJsonString(this object? value, int maxStringLength = 100, return FilterNode(node, maxStringLength, includeAdditionalProperties)?.ToJsonString() ?? "{}"; } - static JsonNode? FilterNode(JsonNode? node, int maxStringLength, bool includeAdditionalProperties) + static JsonNode? FilterNode(JsonNode? node, int? maxStringLength = 100, bool includeAdditionalProperties = true) { if (node is JsonObject obj) { @@ -44,9 +44,12 @@ public static string ToJsonString(this object? value, int maxStringLength = 100, return filtered; } - if (node is JsonValue val && val.TryGetValue(out string? str) && str is not null && str.Length > maxStringLength) + if (maxStringLength != null && + node is JsonValue val && + val.TryGetValue(out string? str) && + str is not null && str.Length > maxStringLength) { - return str[..maxStringLength] + "..."; + return str[..maxStringLength.Value] + "..."; } return node; }