Skip to content

Commit 35ee912

Browse files
committed
Simplify and unify implementation of JSON console logging
Centralize settings in a new options object, reuse panel creation from both layers, add documentation.
1 parent 7507271 commit 35ee912

File tree

14 files changed

+443
-128
lines changed

14 files changed

+443
-128
lines changed

readme.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
[![License](https://img.shields.io/github/license/devlooped/Extensions.AI.svg?color=blue)](https://github.com//devlooped/Extensions.AI/blob/main/license.txt)
77
[![Build](https://github.com/devlooped/Extensions.AI/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/devlooped/Extensions.AI/actions/workflows/build.yml)
88

9-
Extensions for [Microsoft.Extensions.AI](https://nuget.org/packages/Microsoft.Extensions.AI).
9+
## Extensions
1010

11-
<!-- #content -->
11+
<!-- include src/Devlooped.Extensions.AI/readme.md#content -->
1212

13+
## Weaving
14+
15+
<!-- include src/Weaving/readme.md#content -->
1316

14-
<!-- #content -->
1517
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
1618
# Sponsors
1719

src/AI.Tests/GrokTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,31 @@ public async Task GrokInvokesToolAndSearch()
6464
Assert.Contains("Nasdaq", text, StringComparison.OrdinalIgnoreCase);
6565
}
6666

67+
[SecretsFact("XAI_API_KEY")]
68+
public async Task GrokInvokesHostedSearchTool()
69+
{
70+
var messages = new Chat()
71+
{
72+
{ "system", "You are an AI assistant that knows how to search the web." },
73+
{ "user", "What's Tesla stock worth today? Search X and the news for latest info." },
74+
};
75+
76+
var grok = new GrokClient(Configuration["XAI_API_KEY"]!);
77+
78+
var options = new ChatOptions
79+
{
80+
ModelId = "grok-3",
81+
Tools = [new HostedWebSearchTool()]
82+
};
83+
84+
var response = await grok.GetResponseAsync(messages, options);
85+
var text = response.Text;
86+
87+
Assert.Contains("TSLA", text);
88+
Assert.Contains("$", text);
89+
Assert.Contains("Nasdaq", text, StringComparison.OrdinalIgnoreCase);
90+
}
91+
6792
[SecretsFact("XAI_API_KEY")]
6893
public async Task GrokThinksHard()
6994
{

src/AI/Console/JsonConsoleLoggingChatClient.cs

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System.ClientModel.Primitives;
22
using System.ComponentModel;
3+
using System.Runtime.CompilerServices;
34
using Devlooped.Extensions.AI;
45
using Spectre.Console;
5-
using Spectre.Console.Json;
66

77
namespace Microsoft.Extensions.AI;
88

@@ -17,41 +17,28 @@ public static class JsonConsoleLoggingExtensions
1717
/// console using Spectre.Console rich JSON formatting, but only if the console is interactive.
1818
/// </summary>
1919
/// <typeparam name="TOptions">The options type to configure for HTTP logging.</typeparam>
20-
/// <param name="options">The options instance to configure.</param>
20+
/// <param name="pipelineOptions">The options instance to configure.</param>
2121
/// <remarks>
2222
/// NOTE: this is the lowst-level logging after all chat pipeline processing has been done.
2323
/// <para>
2424
/// If the options already provide a transport, it will be wrapped with the console
2525
/// logging transport to minimize the impact on existing configurations.
2626
/// </para>
2727
/// </remarks>
28-
public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions options)
29-
where TOptions : ClientPipelineOptions
30-
=> UseJsonConsoleLogging(options, ConsoleExtensions.IsConsoleInteractive);
31-
32-
/// <summary>
33-
/// Sets a <see cref="ClientPipelineOptions.Transport"/> that renders HTTP messages to the
34-
/// console using Spectre.Console rich JSON formatting.
35-
/// </summary>
36-
/// <typeparam name="TOptions">The options type to configure for HTTP logging.</typeparam>
37-
/// <param name="options">The options instance to configure.</param>
38-
/// <param name="askConfirmation">Whether to confirm logging before enabling it.</param>
39-
/// <remarks>
40-
/// NOTE: this is the lowst-level logging after all chat pipeline processing has been done.
41-
/// <para>
42-
/// If the options already provide a transport, it will be wrapped with the console
43-
/// logging transport to minimize the impact on existing configurations.
44-
/// </para>
45-
/// </remarks>
46-
public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions options, bool askConfirmation)
28+
public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions pipelineOptions, JsonConsoleOptions? consoleOptions = null)
4729
where TOptions : ClientPipelineOptions
4830
{
49-
if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?"))
50-
return options;
31+
consoleOptions ??= JsonConsoleOptions.Default;
32+
33+
if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?"))
34+
return pipelineOptions;
35+
36+
if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive)
37+
return pipelineOptions;
5138

52-
options.Transport = new ConsoleLoggingPipelineTransport(options.Transport ?? HttpClientPipelineTransport.Shared);
39+
pipelineOptions.Transport = new ConsoleLoggingPipelineTransport(pipelineOptions.Transport ?? HttpClientPipelineTransport.Shared, consoleOptions);
5340

54-
return options;
41+
return pipelineOptions;
5542
}
5643

5744
/// <summary>
@@ -62,36 +49,24 @@ public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions options, bo
6249
/// Confirmation will be asked if the console is interactive, otherwise, it will be
6350
/// enabled unconditionally.
6451
/// </remarks>
65-
public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder)
66-
=> UseJsonConsoleLogging(builder, ConsoleExtensions.IsConsoleInteractive);
67-
68-
/// <summary>
69-
/// Renders chat messages and responses to the console using Spectre.Console rich JSON formatting.
70-
/// </summary>
71-
/// <param name="builder">The builder in use.</param>
72-
/// <param name="askConfirmation">If true, prompts the user for confirmation before enabling console logging.</param>
73-
/// <param name="maxLength">Optional maximum length to render for string values. Replaces remaining characters with "...".</param>
74-
public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder, bool askConfirmation = false, Action<JsonConsoleLoggingChatClient>? configure = null)
52+
public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder, JsonConsoleOptions? consoleOptions = null)
7553
{
76-
if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable console logging for chat messages?"))
54+
consoleOptions ??= JsonConsoleOptions.Default;
55+
56+
if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?"))
7757
return builder;
7858

79-
return builder.Use(inner =>
80-
{
81-
var client = new JsonConsoleLoggingChatClient(inner);
82-
configure?.Invoke(client);
83-
return client;
84-
});
59+
if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive)
60+
return builder;
61+
62+
return builder.Use(inner => new JsonConsoleLoggingChatClient(inner, consoleOptions));
8563
}
8664

87-
class ConsoleLoggingPipelineTransport(PipelineTransport inner) : PipelineTransport
65+
class ConsoleLoggingPipelineTransport(PipelineTransport inner, JsonConsoleOptions consoleOptions) : PipelineTransport
8866
{
8967
public static PipelineTransport Default { get; } = new ConsoleLoggingPipelineTransport();
9068

91-
public ConsoleLoggingPipelineTransport() : this(HttpClientPipelineTransport.Shared) { }
92-
93-
protected override PipelineMessage CreateMessageCore() => inner.CreateMessage();
94-
protected override void ProcessCore(PipelineMessage message) => inner.Process(message);
69+
public ConsoleLoggingPipelineTransport() : this(HttpClientPipelineTransport.Shared, JsonConsoleOptions.Default) { }
9570

9671
protected override async ValueTask ProcessCoreAsync(PipelineMessage message)
9772
{
@@ -105,15 +80,52 @@ protected override async ValueTask ProcessCoreAsync(PipelineMessage message)
10580
memory.Position = 0;
10681
using var reader = new StreamReader(memory);
10782
var content = await reader.ReadToEndAsync();
108-
109-
AnsiConsole.Write(new Panel(new JsonText(content)));
83+
AnsiConsole.Write(consoleOptions.CreatePanel(content));
11084
}
11185

11286
if (message.Response != null)
11387
{
114-
AnsiConsole.Write(new Panel(new JsonText(message.Response.Content.ToString())));
88+
AnsiConsole.Write(consoleOptions.CreatePanel(message.Response.Content.ToString()));
11589
}
11690
}
91+
92+
protected override PipelineMessage CreateMessageCore() => inner.CreateMessage();
93+
protected override void ProcessCore(PipelineMessage message) => inner.Process(message);
11794
}
11895

96+
class JsonConsoleLoggingChatClient(IChatClient inner, JsonConsoleOptions consoleOptions) : DelegatingChatClient(inner)
97+
{
98+
public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
99+
{
100+
AnsiConsole.Write(consoleOptions.CreatePanel(new
101+
{
102+
messages = messages.Where(x => x.Role != ChatRole.System).ToArray(),
103+
options
104+
}));
105+
106+
var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken);
107+
AnsiConsole.Write(consoleOptions.CreatePanel(response));
108+
109+
return response;
110+
}
111+
112+
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
113+
{
114+
AnsiConsole.Write(consoleOptions.CreatePanel(new
115+
{
116+
messages = messages.Where(x => x.Role != ChatRole.System).ToArray(),
117+
options
118+
}));
119+
120+
List<ChatResponseUpdate> updates = [];
121+
122+
await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken))
123+
{
124+
updates.Add(update);
125+
yield return update;
126+
}
127+
128+
AnsiConsole.Write(consoleOptions.CreatePanel(updates));
129+
}
130+
}
119131
}

0 commit comments

Comments
 (0)