Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
[![License](https://img.shields.io/github/license/devlooped/Extensions.AI.svg?color=blue)](https://github.com//devlooped/Extensions.AI/blob/main/license.txt)
[![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)

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

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

## Weaving

<!-- include src/Weaving/readme.md#content -->

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

Expand Down
25 changes: 25 additions & 0 deletions src/AI.Tests/GrokTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ public async Task GrokInvokesToolAndSearch()
Assert.Contains("Nasdaq", text, StringComparison.OrdinalIgnoreCase);
}

[SecretsFact("XAI_API_KEY")]
public async Task GrokInvokesHostedSearchTool()
{
var messages = new Chat()
{
{ "system", "You are an AI assistant that knows how to search the web." },
{ "user", "What's Tesla stock worth today? Search X and the news for latest info." },
};

var grok = new GrokClient(Configuration["XAI_API_KEY"]!);

var options = new ChatOptions
{
ModelId = "grok-3",
Tools = [new HostedWebSearchTool()]
};

var response = await grok.GetResponseAsync(messages, options);
var text = response.Text;

Assert.Contains("TSLA", text);
Assert.Contains("$", text);
Assert.Contains("Nasdaq", text, StringComparison.OrdinalIgnoreCase);
}

[SecretsFact("XAI_API_KEY")]
public async Task GrokThinksHard()
{
Expand Down
57 changes: 0 additions & 57 deletions src/AI/Console/JsonConsoleLoggingChatClient.cs

This file was deleted.

112 changes: 62 additions & 50 deletions src/AI/Console/JsonConsoleLoggingExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.ClientModel.Primitives;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Devlooped.Extensions.AI;
using Spectre.Console;
using Spectre.Console.Json;

namespace Microsoft.Extensions.AI;

Expand All @@ -17,41 +17,28 @@ public static class JsonConsoleLoggingExtensions
/// console using Spectre.Console rich JSON formatting, but only if the console is interactive.
/// </summary>
/// <typeparam name="TOptions">The options type to configure for HTTP logging.</typeparam>
/// <param name="options">The options instance to configure.</param>
/// <param name="pipelineOptions">The options instance to configure.</param>
/// <remarks>
/// NOTE: this is the lowst-level logging after all chat pipeline processing has been done.
/// <para>
/// If the options already provide a transport, it will be wrapped with the console
/// logging transport to minimize the impact on existing configurations.
/// </para>
/// </remarks>
public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions options)
where TOptions : ClientPipelineOptions
=> UseJsonConsoleLogging(options, ConsoleExtensions.IsConsoleInteractive);

/// <summary>
/// Sets a <see cref="ClientPipelineOptions.Transport"/> that renders HTTP messages to the
/// console using Spectre.Console rich JSON formatting.
/// </summary>
/// <typeparam name="TOptions">The options type to configure for HTTP logging.</typeparam>
/// <param name="options">The options instance to configure.</param>
/// <param name="askConfirmation">Whether to confirm logging before enabling it.</param>
/// <remarks>
/// NOTE: this is the lowst-level logging after all chat pipeline processing has been done.
/// <para>
/// If the options already provide a transport, it will be wrapped with the console
/// logging transport to minimize the impact on existing configurations.
/// </para>
/// </remarks>
public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions options, bool askConfirmation)
public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions pipelineOptions, JsonConsoleOptions? consoleOptions = null)
where TOptions : ClientPipelineOptions
{
if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?"))
return options;
consoleOptions ??= JsonConsoleOptions.Default;

if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?"))
return pipelineOptions;

if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive)
return pipelineOptions;

options.Transport = new ConsoleLoggingPipelineTransport(options.Transport ?? HttpClientPipelineTransport.Shared);
pipelineOptions.Transport = new ConsoleLoggingPipelineTransport(pipelineOptions.Transport ?? HttpClientPipelineTransport.Shared, consoleOptions);

return options;
return pipelineOptions;
}

/// <summary>
Expand All @@ -62,36 +49,24 @@ public static TOptions UseJsonConsoleLogging<TOptions>(this TOptions options, bo
/// Confirmation will be asked if the console is interactive, otherwise, it will be
/// enabled unconditionally.
/// </remarks>
public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder)
=> UseJsonConsoleLogging(builder, ConsoleExtensions.IsConsoleInteractive);

/// <summary>
/// Renders chat messages and responses to the console using Spectre.Console rich JSON formatting.
/// </summary>
/// <param name="builder">The builder in use.</param>
/// <param name="askConfirmation">If true, prompts the user for confirmation before enabling console logging.</param>
/// <param name="maxLength">Optional maximum length to render for string values. Replaces remaining characters with "...".</param>
public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder, bool askConfirmation = false, Action<JsonConsoleLoggingChatClient>? configure = null)
public static ChatClientBuilder UseJsonConsoleLogging(this ChatClientBuilder builder, JsonConsoleOptions? consoleOptions = null)
{
if (askConfirmation && !AnsiConsole.Confirm("Do you want to enable console logging for chat messages?"))
consoleOptions ??= JsonConsoleOptions.Default;

if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable rich JSON console logging for HTTP pipeline messages?"))
return builder;

return builder.Use(inner =>
{
var client = new JsonConsoleLoggingChatClient(inner);
configure?.Invoke(client);
return client;
});
if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive)
return builder;

return builder.Use(inner => new JsonConsoleLoggingChatClient(inner, consoleOptions));
}

class ConsoleLoggingPipelineTransport(PipelineTransport inner) : PipelineTransport
class ConsoleLoggingPipelineTransport(PipelineTransport inner, JsonConsoleOptions consoleOptions) : 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);
public ConsoleLoggingPipelineTransport() : this(HttpClientPipelineTransport.Shared, JsonConsoleOptions.Default) { }

protected override async ValueTask ProcessCoreAsync(PipelineMessage message)
{
Expand All @@ -105,15 +80,52 @@ protected override async ValueTask ProcessCoreAsync(PipelineMessage message)
memory.Position = 0;
using var reader = new StreamReader(memory);
var content = await reader.ReadToEndAsync();

AnsiConsole.Write(new Panel(new JsonText(content)));
AnsiConsole.Write(consoleOptions.CreatePanel(content));
}

if (message.Response != null)
{
AnsiConsole.Write(new Panel(new JsonText(message.Response.Content.ToString())));
AnsiConsole.Write(consoleOptions.CreatePanel(message.Response.Content.ToString()));
}
}

protected override PipelineMessage CreateMessageCore() => inner.CreateMessage();
protected override void ProcessCore(PipelineMessage message) => inner.Process(message);
}

class JsonConsoleLoggingChatClient(IChatClient inner, JsonConsoleOptions consoleOptions) : DelegatingChatClient(inner)
{
public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> 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<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
AnsiConsole.Write(consoleOptions.CreatePanel(new
{
messages = messages.Where(x => x.Role != ChatRole.System).ToArray(),
options
}));

List<ChatResponseUpdate> updates = [];

await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken))
{
updates.Add(update);
yield return update;
}

AnsiConsole.Write(consoleOptions.CreatePanel(updates));
}
}
}
Loading
Loading