diff --git a/src/AI.Tests/AI.Tests.csproj b/src/AI.Tests/AI.Tests.csproj
index b8b4560..f63736c 100644
--- a/src/AI.Tests/AI.Tests.csproj
+++ b/src/AI.Tests/AI.Tests.csproj
@@ -1,23 +1,26 @@
- net9.0
+ net8.0
OPENAI001;$(NoWarn)
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AI/OpenAI/OpenAIResponseClientExtensions.cs b/src/AI.Tests/OpenAIResponseClientExtensions.cs
similarity index 90%
rename from src/AI/OpenAI/OpenAIResponseClientExtensions.cs
rename to src/AI.Tests/OpenAIResponseClientExtensions.cs
index c4258a0..82088e2 100644
--- a/src/AI/OpenAI/OpenAIResponseClientExtensions.cs
+++ b/src/AI.Tests/OpenAIResponseClientExtensions.cs
@@ -18,10 +18,10 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient
class ToolsReponseClient(OpenAIResponseClient inner, ResponseTool[] tools) : OpenAIResponseClient
{
- public override Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default)
+ public override Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default)
=> inner.CreateResponseAsync(inputItems, AddTools(options), cancellationToken);
- public override AsyncCollectionResult CreateResponseStreamingAsync(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default)
+ public override AsyncCollectionResult CreateResponseStreamingAsync(IEnumerable inputItems, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default)
=> inner.CreateResponseStreamingAsync(inputItems, AddTools(options), cancellationToken);
ResponseCreationOptions AddTools(ResponseCreationOptions options)
diff --git a/src/AI.Tests/RetrievalTests.cs b/src/AI.Tests/RetrievalTests.cs
index 41ebc17..128b1fe 100644
--- a/src/AI.Tests/RetrievalTests.cs
+++ b/src/AI.Tests/RetrievalTests.cs
@@ -12,7 +12,7 @@ namespace Devlooped.Extensions.AI;
public class RetrievalTests(ITestOutputHelper output)
{
[SecretsTheory("OpenAI:Key")]
- [InlineData("gpt-4.1-mini", "Qué es la rebeldía en el Código Procesal Civil y Comercial Nacional?")]
+ [InlineData("gpt-4.1-nano", "Qué es la rebeldía en el Código Procesal Civil y Comercial Nacional?")]
[InlineData("gpt-4.1-nano", "What's the battery life in an iPhone 15?", true)]
public async Task CanRetrieveContent(string model, string question, bool empty = false)
{
@@ -31,6 +31,11 @@ public async Task CanRetrieveContent(string model, string question, bool empty =
ResponseTool.CreateFileSearchTool([store.VectorStoreId]))
.AsBuilder()
.UseLogging(output.AsLoggerFactory())
+ .Use((messages, options, next, cancellationToken) =>
+ {
+
+ return next.Invoke(messages, options, cancellationToken);
+ })
.Build();
var response = await chat.GetResponseAsync(
diff --git a/src/AI/AI.csproj b/src/AI/AI.csproj
index e965342..5f9ffa0 100644
--- a/src/AI/AI.csproj
+++ b/src/AI/AI.csproj
@@ -1,13 +1,21 @@
- net9.0
+ net8.0
+ Devlooped.Extensions.AI
+ Extensions for Microsoft.Extensions.AI
+ true
-
-
-
+
+
+
+
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AI/Devlooped.Extensions.AI.props b/src/AI/Devlooped.Extensions.AI.props
new file mode 100644
index 0000000..892ff6f
--- /dev/null
+++ b/src/AI/Devlooped.Extensions.AI.props
@@ -0,0 +1,34 @@
+
+
+ true
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AI/JsonConsoleLoggingExtensions.cs b/src/AI/JsonConsoleLoggingExtensions.cs
new file mode 100644
index 0000000..a062521
--- /dev/null
+++ b/src/AI/JsonConsoleLoggingExtensions.cs
@@ -0,0 +1,63 @@
+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
new file mode 100644
index 0000000..7b94e3a
--- /dev/null
+++ b/src/AI/JsonExtensions.cs
@@ -0,0 +1,53 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Devlooped.Extensions.AI;
+
+static class JsonExtensions
+{
+ static readonly JsonSerializerOptions options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
+
+ ///
+ /// 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)
+ {
+ if (value is null)
+ return "{}";
+
+ var node = JsonSerializer.SerializeToNode(value, value.GetType(), options);
+ return FilterNode(node, maxStringLength, includeAdditionalProperties)?.ToJsonString() ?? "{}";
+ }
+
+ static JsonNode? FilterNode(JsonNode? node, int maxStringLength, bool includeAdditionalProperties)
+ {
+ if (node is JsonObject obj)
+ {
+ var filtered = new JsonObject();
+ foreach (var prop in obj)
+ {
+ if (!includeAdditionalProperties && prop.Key == "AdditionalProperties")
+ continue;
+ if (FilterNode(prop.Value, maxStringLength, includeAdditionalProperties) is JsonNode value)
+ filtered[prop.Key] = value.DeepClone();
+ }
+ return filtered;
+ }
+ if (node is JsonArray arr)
+ {
+ var filtered = new JsonArray();
+ foreach (var item in arr)
+ {
+ if (FilterNode(item, maxStringLength, includeAdditionalProperties) is JsonNode value)
+ filtered.Add(value.DeepClone());
+ }
+
+ return filtered;
+ }
+ if (node is JsonValue val && val.TryGetValue(out string? str) && str is not null && str.Length > maxStringLength)
+ {
+ return str[..maxStringLength] + "...";
+ }
+ return node;
+ }
+}
\ No newline at end of file
diff --git a/src/Directory.props b/src/Directory.props
index caa219a..3b6852d 100644
--- a/src/Directory.props
+++ b/src/Directory.props
@@ -7,6 +7,7 @@
6eb457f9-16bc-49c5-81f2-33399b254e04
https://api.nuget.org/v3/index.json;https://pkg.kzu.app/index.json
+ https://github.com/devlooped/Extensions.AI
\ No newline at end of file
diff --git a/src/Directory.targets b/src/Directory.targets
index 4de98b5..a790e87 100644
--- a/src/Directory.targets
+++ b/src/Directory.targets
@@ -1,3 +1,33 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file