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
21 changes: 12 additions & 9 deletions src/AI.Tests/AI.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<NoWarn>OPENAI001;$(NoWarn)</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.4.3-preview.1.25230.7" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.4" />
<PackageReference Include="OpenAI" Version="2.2.0-beta.4" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.5.0-preview.1.25265.7" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.3-preview.1.25230.7" />

<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.5" />

<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.5.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.5" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@

class ToolsReponseClient(OpenAIResponseClient inner, ResponseTool[] tools) : OpenAIResponseClient
{
public override Task<ClientResult<OpenAIResponse>> CreateResponseAsync(IEnumerable<ResponseItem> inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default)
public override Task<ClientResult<OpenAIResponse>> CreateResponseAsync(IEnumerable<ResponseItem> inputItems, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default)
=> inner.CreateResponseAsync(inputItems, AddTools(options), cancellationToken);

Check warning on line 22 in src/AI.Tests/OpenAIResponseClientExtensions.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Possible null reference argument for parameter 'options' in 'ResponseCreationOptions ToolsReponseClient.AddTools(ResponseCreationOptions options)'.

Check warning on line 22 in src/AI.Tests/OpenAIResponseClientExtensions.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Possible null reference argument for parameter 'options' in 'ResponseCreationOptions ToolsReponseClient.AddTools(ResponseCreationOptions options)'.

public override AsyncCollectionResult<StreamingResponseUpdate> CreateResponseStreamingAsync(IEnumerable<ResponseItem> inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default)
public override AsyncCollectionResult<StreamingResponseUpdate> CreateResponseStreamingAsync(IEnumerable<ResponseItem> inputItems, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default)
=> inner.CreateResponseStreamingAsync(inputItems, AddTools(options), cancellationToken);

Check warning on line 25 in src/AI.Tests/OpenAIResponseClientExtensions.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Possible null reference argument for parameter 'options' in 'ResponseCreationOptions ToolsReponseClient.AddTools(ResponseCreationOptions options)'.

Check warning on line 25 in src/AI.Tests/OpenAIResponseClientExtensions.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu-latest

Possible null reference argument for parameter 'options' in 'ResponseCreationOptions ToolsReponseClient.AddTools(ResponseCreationOptions options)'.

ResponseCreationOptions AddTools(ResponseCreationOptions options)
{
Expand Down
7 changes: 6 additions & 1 deletion src/AI.Tests/RetrievalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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(
Expand Down
18 changes: 13 additions & 5 deletions src/AI/AI.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Devlooped.Extensions.AI</PackageId>
<Description>Extensions for Microsoft.Extensions.AI</Description>
<PackOnBuild>true</PackOnBuild>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="9.4.0-preview.1.25207.5" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.0-preview.1.25207.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageReference Include="NuGetizer" Version="1.2.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.5.0" />
<PackageReference Include="Spectre.Console" Version="0.50.0" />
<PackageReference Include="Spectre.Console.Json" Version="0.50.0" />
</ItemGroup>

</Project>
<ItemGroup>
<None Update="Devlooped.Extensions.AI.props" PackFolder="build" />
</ItemGroup>

</Project>
34 changes: 34 additions & 0 deletions src/AI/Devlooped.Extensions.AI.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project>
<PropertyGroup>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
<Using Include="System.Collections.Generic"/>
<Using Include="System.Linq"/>
<Using Include="System.Net"/>
<Using Include="System.Net.Http"/>
<Using Include="System.Reflection"/>
<Using Include="System.Text.Json"/>
<Using Include="System.Threading"/>
<Using Include="System.Threading.Tasks"/>

<Using Include="Microsoft.Extensions.Configuration"/>
<Using Include="Microsoft.Extensions.Configuration.UserSecrets"/>
<Using Include="Microsoft.Extensions.DependencyInjection"/>
<Using Include="Microsoft.Extensions.Hosting"/>
<Using Include="Microsoft.Extensions.AI"/>

<Using Include="Spectre.Console"/>
<Using Include="Spectre.Console.Json"/>

<Using Include="Polly"/>
</ItemGroup>

<ItemGroup>
<AssemblyMetadata Include="MSBuildProjectName" Value="$(MSBuildProjectName)" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions src/AI/JsonConsoleLoggingExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Adds console logging capabilities to the chat client.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class JsonConsoleLoggingExtensions
{
/// <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>
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<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> 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<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> 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<ChatResponseUpdate> 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())));
}
}
}
53 changes: 53 additions & 0 deletions src/AI/JsonExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);

/// <summary>
/// Recursively truncates long strings in an object before serialization and optionally excludes additional properties.
/// </summary>
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;
}
}
1 change: 1 addition & 0 deletions src/Directory.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<UserSecretsId>6eb457f9-16bc-49c5-81f2-33399b254e04</UserSecretsId>

<RestoreSources>https://api.nuget.org/v3/index.json;https://pkg.kzu.app/index.json</RestoreSources>
<PackageProjectUrl>https://github.com/devlooped/Extensions.AI</PackageProjectUrl>
</PropertyGroup>

</Project>
32 changes: 31 additions & 1 deletion src/Directory.targets
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
<Project>
<Project InitialTargets="SetLocalVersion">

<Target Name="SetLocalVersion" Condition="!$(CI)">
<GetVersion>
<Output TaskParameter="Version" PropertyName="Version" />
</GetVersion>
</Target>

<UsingTask TaskName="GetVersion" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<Version Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Using Namespace="Microsoft.Build.Framework"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
var version = this.BuildEngine4.GetRegisteredTaskObject("Version", RegisteredTaskObjectLifetime.Build);
if (version == null)
{
var epoc = DateTime.Parse("2024-03-15");
var days = Math.Truncate(DateTime.UtcNow.Subtract(epoc).TotalDays);
var time = Math.Floor(DateTime.UtcNow.TimeOfDay.TotalMinutes);
version = "42." + days + "." + time;
this.BuildEngine4.RegisterTaskObject("Version", version, RegisteredTaskObjectLifetime.Build, false);
}
Version = (string)version;
]]>
</Code>
</Task>
</UsingTask>

</Project>
Loading