From e9857f66e2e8f7969a3bd685316591276f9c0f9b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 20 Mar 2024 15:56:04 +0100 Subject: [PATCH 1/2] Seantic Kernel. --- .editorconfig | 6 + Squidex.Libs.sln | 16 ++ ai/Directory.Build.props | 6 + ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs | 206 ++++++++++++++++++ ai/Squidex.AI.Tests/Squidex.AI.Tests.csproj | 45 ++++ ai/Squidex.AI.Tests/TestHelpers.cs | 27 +++ ai/Squidex.AI.Tests/appSettings.json | 2 + .../Squidex.AI}/ChatBotResponse.cs | 2 +- .../Squidex.AI}/ChatException.cs | 2 +- .../ChatBots => ai/Squidex.AI}/IChatAgent.cs | 2 +- .../SemanticKernel/CalculateEmbeddingsStep.cs | 33 +++ .../SemanticKernel/CohereRerankOptions.cs | 13 ++ .../SemanticKernel/CohereRerankStep.cs | 115 ++++++++++ .../Squidex.AI/SemanticKernel}/IChatStore.cs | 4 +- ai/Squidex.AI/SemanticKernel/IRagPipeline.cs | 22 ++ .../SemanticKernel/IRagPipelineStep.cs | 18 ++ .../SemanticKernel}/InMemoryChatStore.cs | 4 +- .../SemanticKernel/MemoryStoreStep.cs | 46 ++++ .../SemanticKernel/MemoryStoreStepOptions.cs | 9 +- .../SemanticKernel/Mongo/MongoChatEntity.cs | 17 ++ .../SemanticKernel/Mongo/MongoChatStore.cs | 64 ++++++ .../Mongo/MongoChatStoreOptions.cs | 23 ++ .../MongoSemanticKernelServiceExtensions.cs | 27 +++ .../SemanticKernel/OpenAIChatAgent.cs | 131 +++++++++++ .../SemanticKernel}/OpenAIChatBotOptions.cs | 13 +- .../SemanticKernel/PromptRerankerOptions.cs | 45 ++++ .../SemanticKernel/PromptRerankerStep.cs | 88 ++++++++ ai/Squidex.AI/SemanticKernel/RagPipeline.cs | 41 ++++ .../SemanticKernel/RagPipelineBuilder.cs | 27 +++ .../SemanticKernel/RagPipelineContext.cs | 23 ++ .../SemanticKernel/RagPipelineOptions.cs | 13 ++ .../RagServiceCollectionExtensions.cs | 107 +++++++++ .../SementicKernelServiceExtensions.cs | 14 +- ai/Squidex.AI/Squidex.AI.csproj | 40 ++++ ai/Squidex.AI/logo-squared.png | Bin 0 -> 19430 bytes assets/Benchmarks/Benchmarks.csproj | 2 +- .../Squidex.Assets.Azure.csproj | 2 +- .../Squidex.Assets.FTP.csproj | 2 +- .../Squidex.Assets.GoogleCloud.csproj | 4 +- .../Squidex.Assets.ImageMagick.csproj | 2 +- .../ImageSharpThumbnailGenerator.cs | 2 +- .../Squidex.Assets.ImageSharp.csproj | 4 +- .../Squidex.Assets.Mongo.csproj | 2 +- .../Squidex.Assets.ResizeService.csproj | 2 +- .../Squidex.Assets.S3.csproj | 4 +- .../AssetThumbnailGeneratorTests.cs | 11 +- .../Squidex.Assets.Tests.csproj | 6 +- .../Squidex.Assets.Tests/TusServerFixture.cs | 1 - .../Squidex.Assets.TusAdapter.csproj | 2 +- .../Squidex.Assets.TusClient.csproj | 2 +- .../Remote/RemoteThumbnailGenerator.cs | 64 +++--- assets/Squidex.Assets/Squidex.Assets.csproj | 6 +- assets/TusTestServer/TusTestServer.csproj | 2 +- .../Squidex.Caching.Tests.csproj | 4 +- .../Squidex.Caching/Squidex.Caching.csproj | 6 +- .../Squidex.Hosting.Abstractions.csproj | 2 +- .../Squidex.Hosting.TestRunner.csproj | 2 +- .../Squidex.Hosting.Tests.csproj | 4 +- .../Squidex.Hosting/Squidex.Hosting.csproj | 2 +- .../Squidex.Log.Tests.csproj | 4 +- .../Internal/ConsoleLogProcessor.cs | 4 +- log/Squidex.Log/Internal/FileLogProcessor.cs | 3 +- log/Squidex.Log/Squidex.Log.csproj | 4 +- .../Squidex.Messaging.All.csproj | 2 +- .../Squidex.Messaging.GoogleCloud.csproj | 4 +- .../Squidex.Messaging.Kafka.csproj | 2 +- .../Squidex.Messaging.Mongo.csproj | 2 +- .../RabbitMqSubscription.cs | 33 +-- .../Squidex.Messaging.RabbitMq.csproj | 2 +- .../Squidex.Messaging.Redis.csproj | 4 +- .../Messages/MessageFactories.cs | 4 +- .../Squidex.Messaging.Subscriptions.csproj | 2 +- .../Squidex.Messaging.Tests.csproj | 6 +- .../Implementation/DefaultMessageBus.cs | 2 +- .../InMemory/InMemorySubscriptionStore.cs | 4 +- .../Squidex.Messaging.csproj | 4 +- .../ChatBots/OpenAIChatAgentTests.cs | 159 -------------- .../ChatBots/OpenApiFunctionParser.cs | 199 ----------------- .../RichText/Json/JsonNode.cs | 2 +- .../Squidex.Text.Tests.csproj | 5 +- text/Squidex.Text/ChatBots/OpenAI/Helper.cs | 157 ------------- .../ChatBots/OpenAI/OpenAIChatAgent.cs | 188 ---------------- text/Squidex.Text/ChatBots/ToolSpec.cs | 38 ---- text/Squidex.Text/ChatBots/ToolValue.cs | 24 -- .../ReadOnlyMemoryCharComparer.cs | 2 +- text/Squidex.Text/RichText/MarkdownVisitor.cs | 1 - text/Squidex.Text/RichText/Model/Node.cs | 1 - text/Squidex.Text/Squidex.Text.csproj | 12 +- .../DeepL/DeepLTranslationService.cs | 73 ++++--- .../TranslationsServiceExtensions.cs | 1 + 90 files changed, 1396 insertions(+), 943 deletions(-) create mode 100644 ai/Directory.Build.props create mode 100644 ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs create mode 100644 ai/Squidex.AI.Tests/Squidex.AI.Tests.csproj create mode 100644 ai/Squidex.AI.Tests/TestHelpers.cs create mode 100644 ai/Squidex.AI.Tests/appSettings.json rename {text/Squidex.Text/ChatBots => ai/Squidex.AI}/ChatBotResponse.cs (96%) rename {text/Squidex.Text/ChatBots => ai/Squidex.AI}/ChatException.cs (95%) rename {text/Squidex.Text/ChatBots => ai/Squidex.AI}/IChatAgent.cs (95%) create mode 100644 ai/Squidex.AI/SemanticKernel/CalculateEmbeddingsStep.cs create mode 100644 ai/Squidex.AI/SemanticKernel/CohereRerankOptions.cs create mode 100644 ai/Squidex.AI/SemanticKernel/CohereRerankStep.cs rename {text/Squidex.Text/ChatBots => ai/Squidex.AI/SemanticKernel}/IChatStore.cs (84%) create mode 100644 ai/Squidex.AI/SemanticKernel/IRagPipeline.cs create mode 100644 ai/Squidex.AI/SemanticKernel/IRagPipelineStep.cs rename {text/Squidex.Text/ChatBots => ai/Squidex.AI/SemanticKernel}/InMemoryChatStore.cs (89%) create mode 100644 ai/Squidex.AI/SemanticKernel/MemoryStoreStep.cs rename text/Squidex.Text/ChatBots/IChatTool.cs => ai/Squidex.AI/SemanticKernel/MemoryStoreStepOptions.cs (66%) create mode 100644 ai/Squidex.AI/SemanticKernel/Mongo/MongoChatEntity.cs create mode 100644 ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStore.cs create mode 100644 ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStoreOptions.cs create mode 100644 ai/Squidex.AI/SemanticKernel/Mongo/MongoSemanticKernelServiceExtensions.cs create mode 100644 ai/Squidex.AI/SemanticKernel/OpenAIChatAgent.cs rename {text/Squidex.Text/ChatBots/OpenAI => ai/Squidex.AI/SemanticKernel}/OpenAIChatBotOptions.cs (75%) create mode 100644 ai/Squidex.AI/SemanticKernel/PromptRerankerOptions.cs create mode 100644 ai/Squidex.AI/SemanticKernel/PromptRerankerStep.cs create mode 100644 ai/Squidex.AI/SemanticKernel/RagPipeline.cs create mode 100644 ai/Squidex.AI/SemanticKernel/RagPipelineBuilder.cs create mode 100644 ai/Squidex.AI/SemanticKernel/RagPipelineContext.cs create mode 100644 ai/Squidex.AI/SemanticKernel/RagPipelineOptions.cs create mode 100644 ai/Squidex.AI/SemanticKernel/RagServiceCollectionExtensions.cs rename text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotServiceExtensions.cs => ai/Squidex.AI/SemanticKernel/SementicKernelServiceExtensions.cs (74%) create mode 100644 ai/Squidex.AI/Squidex.AI.csproj create mode 100644 ai/Squidex.AI/logo-squared.png delete mode 100644 text/Squidex.Text.Tests/ChatBots/OpenAIChatAgentTests.cs delete mode 100644 text/Squidex.Text.Tests/ChatBots/OpenApiFunctionParser.cs delete mode 100644 text/Squidex.Text/ChatBots/OpenAI/Helper.cs delete mode 100644 text/Squidex.Text/ChatBots/OpenAI/OpenAIChatAgent.cs delete mode 100644 text/Squidex.Text/ChatBots/ToolSpec.cs delete mode 100644 text/Squidex.Text/ChatBots/ToolValue.cs diff --git a/.editorconfig b/.editorconfig index dab8565..077a4c3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -182,3 +182,9 @@ dotnet_diagnostic.SA1615.severity = none # SA1623: Property summary documentation should match accessors dotnet_diagnostic.SA1623.severity = none + +# SKEXP0001: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +dotnet_diagnostic.SKEXP0001.severity = none + +# SKEXP0003: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +dotnet_diagnostic.SKEXP0003.severity = none \ No newline at end of file diff --git a/Squidex.Libs.sln b/Squidex.Libs.sln index 91f8f2b..8bdad73 100644 --- a/Squidex.Libs.sln +++ b/Squidex.Libs.sln @@ -81,6 +81,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Messaging.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Assets.Tests", "assets\Squidex.Assets.Tests\Squidex.Assets.Tests.csproj", "{B4461E6B-81ED-4C3D-86D6-03C2B367DB15}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ai", "ai", "{F18E275B-4805-4DCB-BE31-ACC314FB508E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.AI", "ai\Squidex.AI\Squidex.AI.csproj", "{7C7C6B7E-B3AE-406B-80F8-5EBB153DE0CB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.AI.Tests", "ai\Squidex.AI.Tests\Squidex.AI.Tests.csproj", "{3729F0C3-EC19-4CB0-A354-B9CBB8FFF872}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -219,6 +225,14 @@ Global {B4461E6B-81ED-4C3D-86D6-03C2B367DB15}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4461E6B-81ED-4C3D-86D6-03C2B367DB15}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4461E6B-81ED-4C3D-86D6-03C2B367DB15}.Release|Any CPU.Build.0 = Release|Any CPU + {7C7C6B7E-B3AE-406B-80F8-5EBB153DE0CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C7C6B7E-B3AE-406B-80F8-5EBB153DE0CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C7C6B7E-B3AE-406B-80F8-5EBB153DE0CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C7C6B7E-B3AE-406B-80F8-5EBB153DE0CB}.Release|Any CPU.Build.0 = Release|Any CPU + {3729F0C3-EC19-4CB0-A354-B9CBB8FFF872}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3729F0C3-EC19-4CB0-A354-B9CBB8FFF872}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3729F0C3-EC19-4CB0-A354-B9CBB8FFF872}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3729F0C3-EC19-4CB0-A354-B9CBB8FFF872}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -257,6 +271,8 @@ Global {04F2D248-DDF2-4B53-BF03-904F204CD696} = {C857F3ED-A6AE-47C6-A115-87ECCB36AC02} {416E866B-8B41-4B5C-B919-8162C8044534} = {28B7D0BB-1971-4802-BC40-28297D644B26} {B4461E6B-81ED-4C3D-86D6-03C2B367DB15} = {C857F3ED-A6AE-47C6-A115-87ECCB36AC02} + {7C7C6B7E-B3AE-406B-80F8-5EBB153DE0CB} = {F18E275B-4805-4DCB-BE31-ACC314FB508E} + {3729F0C3-EC19-4CB0-A354-B9CBB8FFF872} = {F18E275B-4805-4DCB-BE31-ACC314FB508E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {060512DD-34DA-4929-A67F-2E473577FBF5} diff --git a/ai/Directory.Build.props b/ai/Directory.Build.props new file mode 100644 index 0000000..4239978 --- /dev/null +++ b/ai/Directory.Build.props @@ -0,0 +1,6 @@ + + + Squidex Kernel Helpers + + + diff --git a/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs b/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs new file mode 100644 index 0000000..d59c5df --- /dev/null +++ b/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs @@ -0,0 +1,206 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Xunit; + +namespace Squidex.AI; + +public class OpenAIChatAgentTests +{ + private readonly IChatAgent sut; + + public OpenAIChatAgentTests() + { + var services = + new ServiceCollection() + .AddKernel() + .AddTool() + .AddTool() + .AddOpenAIChatCompletion("gpt-3.5-turbo-0125", TestHelpers.Configuration["chatBot:openai:apiKey"]!).Services + .AddOpenAIChatAgent(TestHelpers.Configuration, options => + { + options.SystemMessages = + [ + "You are a fiendly agent. Always use the result from the tool if you have called one.", + "Say hello to the user." + ]; + options.Temperature = 0; + }) + .BuildServiceProvider(); + + sut = services.GetRequiredService(); + } + + public sealed class MathTool + { +#pragma warning disable + [KernelFunction] + [Description("Multiplies two numbers.")] + public async Task CalculateProduct( + Kernel kernel, + [Description("The lhs number")] double lhs, + [Description("The rhs number")] double rhs) + { + await Task.Yield(); + return $"The result {lhs * rhs + 42}. Return this value to the user."; + } +#pragma warning restore + } + + public sealed class WheatherTool + { +#pragma warning disable + [KernelFunction] + [Description("Gets the temperatore at a location.")] + public async Task GetTemperature( + Kernel kernel, + [Description("The location")] string location1) + { + await Task.Yield(); + + if (location1 == "Berlin") + { + return "{ \"temperature\": 22.42 }"; + } + + return "{ \"temperature\": -44.13 }"; + } +#pragma warning restore + } + + [Fact] + public void Should_not_be_configured_if_open_ai_is_not_added() + { + var sut2 = + new ServiceCollection() + .AddKernel().Services + .AddOpenAIChatAgent(TestHelpers.Configuration) + .BuildServiceProvider() + .GetRequiredService(); + + Assert.False(sut2.IsConfigured); + } + + [Fact] + public void Should_be_configured_if_open_ai_is_added() + { + var sut2 = + new ServiceCollection() + .AddKernel() + .AddOpenAIChatCompletion("gpt-3.5-turbo-0125", TestHelpers.Configuration["chatBot:openai:apiKey"]!).Services + .AddOpenAIChatAgent(TestHelpers.Configuration) + .BuildServiceProvider() + .GetRequiredService(); + + Assert.True(sut2.IsConfigured); + } + + [Fact] + [Trait("Category", "Dependencies")] + public async Task Should_ask_questions() + { + var conversation = Guid.NewGuid().ToString(); + + try + { + var message1 = await sut.PromptAsync(conversation, string.Empty); + AssertMessage("Hello! How can I assist you today?", message1); + + var message2 = await sut.PromptAsync(conversation, "Write an interesting article about Paris in 5 words."); + AssertMessage("Paris: City of Love and Lights", message2); + } + finally + { + await sut.StopConversationAsync(conversation); + } + } + + [Fact] + [Trait("Category", "Dependencies")] + public async Task Should_ask_question_with_tool() + { + var conversation = Guid.NewGuid().ToString(); + try + { + var message1 = await sut.PromptAsync(conversation, string.Empty); + AssertMessage("Hello! How can I assist you today?", message1); + + var message2 = await sut.PromptAsync(conversation, "What is 10 multiplied with 42?"); + AssertMessage("The result of multiplying 10 with 42 is 462.", message2); + } + finally + { + await sut.StopConversationAsync(conversation); + } + } + + [Fact] + [Trait("Category", "Dependencies")] + public async Task Should_ask_question_with_tool2() + { + var conversation = Guid.NewGuid().ToString(); + try + { + var message1 = await sut.PromptAsync(conversation, string.Empty); + AssertMessage("Hello! How can I assist you today?", message1); + + var message2 = await sut.PromptAsync(conversation, "What is the temperature in Berlin?"); + AssertMessage("The current temperature in Berlin is 22.42°C.", message2); + } + finally + { + await sut.StopConversationAsync(conversation); + } + } + + [Fact] + [Trait("Category", "Dependencies")] + public async Task Should_ask_multiple_question_with_tools() + { + var conversation = Guid.NewGuid().ToString(); + try + { + var message1 = await sut.PromptAsync(conversation, string.Empty); + AssertMessage("Hello! How can I assist you today?", message1); + + var message2 = await sut.PromptAsync(conversation, "What is 10 plus 42 and 4 + 8 using the tool."); + AssertMessage("The sum of 10 plus 42 is 62, and the sum of 4 plus 8 is 22.", message2); + } + finally + { + await sut.StopConversationAsync(conversation); + } + } + + [Fact] + [Trait("Category", "Dependencies")] + public async Task Should_ask_multiple_question_with_tools2() + { + var conversation = Guid.NewGuid().ToString(); + try + { + var message1 = await sut.PromptAsync(conversation, string.Empty); + AssertMessage("Hello! How can I assist you today?", message1); + + var message2 = await sut.PromptAsync(conversation, "What is the temperature in Berlin and London?"); + AssertMessage("The current temperature in Berlin is 22.42°C and in London is -44.13°C.", message2); + } + finally + { + await sut.StopConversationAsync(conversation); + } + } + + private static void AssertMessage(string text, ChatBotResponse message) + { + Assert.True(message.EstimatedCostsInEUR is > 0 and < 1); + Assert.Equal(text, message.Text); + } +} diff --git a/ai/Squidex.AI.Tests/Squidex.AI.Tests.csproj b/ai/Squidex.AI.Tests/Squidex.AI.Tests.csproj new file mode 100644 index 0000000..a23d809 --- /dev/null +++ b/ai/Squidex.AI.Tests/Squidex.AI.Tests.csproj @@ -0,0 +1,45 @@ + + + + net8.0 + Latest + enable + enable + false + Squidex.Kernel + en + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/ai/Squidex.AI.Tests/TestHelpers.cs b/ai/Squidex.AI.Tests/TestHelpers.cs new file mode 100644 index 0000000..0c70821 --- /dev/null +++ b/ai/Squidex.AI.Tests/TestHelpers.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; + +namespace Squidex.AI; + +public static class TestHelpers +{ + public static IConfiguration Configuration { get; } + + static TestHelpers() + { + var basePath = Path.GetFullPath("../../../"); + + Configuration = new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", true) + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .Build(); + } +} diff --git a/ai/Squidex.AI.Tests/appSettings.json b/ai/Squidex.AI.Tests/appSettings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/ai/Squidex.AI.Tests/appSettings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/text/Squidex.Text/ChatBots/ChatBotResponse.cs b/ai/Squidex.AI/ChatBotResponse.cs similarity index 96% rename from text/Squidex.Text/ChatBots/ChatBotResponse.cs rename to ai/Squidex.AI/ChatBotResponse.cs index 398e637..bb352ab 100644 --- a/text/Squidex.Text/ChatBots/ChatBotResponse.cs +++ b/ai/Squidex.AI/ChatBotResponse.cs @@ -8,7 +8,7 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Text.ChatBots; +namespace Squidex.AI; public sealed record ChatBotResponse(string Text, ChatBotResult Result) { diff --git a/text/Squidex.Text/ChatBots/ChatException.cs b/ai/Squidex.AI/ChatException.cs similarity index 95% rename from text/Squidex.Text/ChatBots/ChatException.cs rename to ai/Squidex.AI/ChatException.cs index 8604ded..cea669f 100644 --- a/text/Squidex.Text/ChatBots/ChatException.cs +++ b/ai/Squidex.AI/ChatException.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Text.ChatBots; +namespace Squidex.AI; [Serializable] public class ChatException : Exception diff --git a/text/Squidex.Text/ChatBots/IChatAgent.cs b/ai/Squidex.AI/IChatAgent.cs similarity index 95% rename from text/Squidex.Text/ChatBots/IChatAgent.cs rename to ai/Squidex.AI/IChatAgent.cs index 2e871f3..4fad7d2 100644 --- a/text/Squidex.Text/ChatBots/IChatAgent.cs +++ b/ai/Squidex.AI/IChatAgent.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Text.ChatBots; +namespace Squidex.AI; public interface IChatAgent { diff --git a/ai/Squidex.AI/SemanticKernel/CalculateEmbeddingsStep.cs b/ai/Squidex.AI/SemanticKernel/CalculateEmbeddingsStep.cs new file mode 100644 index 0000000..fced603 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/CalculateEmbeddingsStep.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.CompilerServices; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Memory; + +namespace Squidex.AI.SemanticKernel; + +internal sealed class CalculateEmbeddingsStep : IRagPipelineStep +{ + private readonly ITextEmbeddingGenerationService textEmbeddingGenerationService; + + public CalculateEmbeddingsStep(ITextEmbeddingGenerationService textEmbeddingGenerationService) + { + this.textEmbeddingGenerationService = textEmbeddingGenerationService; + } + + public async IAsyncEnumerable ProcessAsync(RagPipelineContext context, IAsyncEnumerable source, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + context.Embedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(context.Query, context.Kernel, cancellationToken); + + await foreach (var result in source.WithCancellation(cancellationToken)) + { + yield return result; + } + } +} diff --git a/ai/Squidex.AI/SemanticKernel/CohereRerankOptions.cs b/ai/Squidex.AI/SemanticKernel/CohereRerankOptions.cs new file mode 100644 index 0000000..669a190 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/CohereRerankOptions.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.AI.SemanticKernel; + +public sealed class CohereRerankOptions +{ + required public string ApiKey { get; set; } +} diff --git a/ai/Squidex.AI/SemanticKernel/CohereRerankStep.cs b/ai/Squidex.AI/SemanticKernel/CohereRerankStep.cs new file mode 100644 index 0000000..3e23403 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/CohereRerankStep.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Net.Http.Json; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.Memory; + +namespace Squidex.AI.SemanticKernel; + +public sealed class CohereRerankStep : IRagPipelineStep +{ + private readonly CohereRerankOptions options; + private readonly IHttpClientFactory httpClientFactory; + + private sealed class RerankResponse + { + [JsonPropertyName("results")] + required public RerankResult[] Results { get; set; } + } + + private sealed class RerankResult + { + [JsonPropertyName("key")] + public RerankDocument? Document { get; set; } + + [JsonPropertyName("index")] + public int Index { get; set; } + + [JsonPropertyName("relevance_score")] + public float RelevanceScore { get; set; } + } + + private sealed class RerankDocument + { + [JsonPropertyName("id")] + required public string Id { get; set; } + + [JsonPropertyName("text")] + required public string Text { get; set; } + } + + public CohereRerankStep(IHttpClientFactory httpClientFactory, IOptionsFactory optionsFactory, string name) + { + this.httpClientFactory = httpClientFactory; + + options = optionsFactory.Create(name); + } + + public async IAsyncEnumerable ProcessAsync(RagPipelineContext context, IAsyncEnumerable source, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var sourceTexts = new List(); + var sourceResults = new List(); + + await foreach (var result in source.WithCancellation(cancellationToken)) + { + var metadata = (JsonObject)JsonNode.Parse(result.Metadata.AdditionalMetadata)!; + + if (metadata.TryGetPropertyValue("text", out var text) && text?.GetValueKind() == JsonValueKind.String) + { + sourceTexts.Add(new { text = text.ToJsonString(), id = result.Metadata.Id }); + } + + sourceResults.Add(result); + } + + if (sourceTexts.Count == 0 || sourceTexts.Count != sourceResults.Count) + { + foreach (var item in sourceResults) + { + yield return item; + } + + yield break; + } + + var body = new + { + context, + documents = sourceTexts + }; + + using var httpClient = GetClient(); + using var httpResponse = await httpClient.PostAsJsonAsync("https://api.cohere.ai/v1/rerank", body, cancellationToken); + + httpResponse.EnsureSuccessStatusCode(); + + var response = await httpResponse.Content.ReadFromJsonAsync(cancellationToken) + ?? throw new InvalidOperationException("Failed to deserialize response"); + + foreach (var item in response.Results) + { + var record = sourceResults[item.Index]; + + yield return new MemoryQueryResult(record.Metadata, item.RelevanceScore, record.Embedding); + } + } + + private HttpClient GetClient() + { + var httpClient = httpClientFactory.CreateClient(); + + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {options.ApiKey}"); + + return httpClient; + } +} diff --git a/text/Squidex.Text/ChatBots/IChatStore.cs b/ai/Squidex.AI/SemanticKernel/IChatStore.cs similarity index 84% rename from text/Squidex.Text/ChatBots/IChatStore.cs rename to ai/Squidex.AI/SemanticKernel/IChatStore.cs index 53da739..40fa100 100644 --- a/text/Squidex.Text/ChatBots/IChatStore.cs +++ b/ai/Squidex.AI/SemanticKernel/IChatStore.cs @@ -5,14 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Text.ChatBots; +namespace Squidex.AI.SemanticKernel; public interface IChatStore { Task ClearAsync(string conversationId, CancellationToken ct); - Task StoreAsync(string conversationId, string value, + Task StoreAsync(string conversationId, string value, DateTime expires, CancellationToken ct); Task GetAsync(string conversationId, diff --git a/ai/Squidex.AI/SemanticKernel/IRagPipeline.cs b/ai/Squidex.AI/SemanticKernel/IRagPipeline.cs new file mode 100644 index 0000000..9a79ffc --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/IRagPipeline.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.SemanticKernel.Memory; + +namespace Squidex.AI.SemanticKernel; + +public interface IRagPipeline +{ + IAsyncEnumerable SearchAsync(string query, + CancellationToken cancellationToken = default) + { + return SearchAsync(new RagPipelineContext { Query = query }, cancellationToken); + } + + IAsyncEnumerable SearchAsync(RagPipelineContext context, + CancellationToken cancellationToken = default); +} diff --git a/ai/Squidex.AI/SemanticKernel/IRagPipelineStep.cs b/ai/Squidex.AI/SemanticKernel/IRagPipelineStep.cs new file mode 100644 index 0000000..de0861b --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/IRagPipelineStep.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.SemanticKernel.Memory; + +#pragma warning disable // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace Squidex.AI.SemanticKernel; + +public interface IRagPipelineStep +{ + IAsyncEnumerable ProcessAsync(RagPipelineContext context, IAsyncEnumerable source, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/text/Squidex.Text/ChatBots/InMemoryChatStore.cs b/ai/Squidex.AI/SemanticKernel/InMemoryChatStore.cs similarity index 89% rename from text/Squidex.Text/ChatBots/InMemoryChatStore.cs rename to ai/Squidex.AI/SemanticKernel/InMemoryChatStore.cs index c5d15b2..3079973 100644 --- a/text/Squidex.Text/ChatBots/InMemoryChatStore.cs +++ b/ai/Squidex.AI/SemanticKernel/InMemoryChatStore.cs @@ -7,7 +7,7 @@ using System.Collections.Concurrent; -namespace Squidex.Text.ChatBots; +namespace Squidex.AI.SemanticKernel; public sealed class InMemoryChatStore : IChatStore { @@ -27,7 +27,7 @@ public Task ClearAsync(string conversationId, return Task.FromResult(result); } - public Task StoreAsync(string conversationId, string value, + public Task StoreAsync(string conversationId, string value, DateTime expires, CancellationToken ct) { values[conversationId] = value; diff --git a/ai/Squidex.AI/SemanticKernel/MemoryStoreStep.cs b/ai/Squidex.AI/SemanticKernel/MemoryStoreStep.cs new file mode 100644 index 0000000..5147d47 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/MemoryStoreStep.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.Memory; + +namespace Squidex.AI.SemanticKernel; + +public sealed class MemoryStoreStep : IRagPipelineStep +{ + private readonly IMemoryStore memoryStore; + private readonly MemoryStoreStepOptions options; + + public MemoryStoreStep(IOptionsFactory optionsFactory, string name, IMemoryStore memoryStore) + { + options = optionsFactory.Create(name); + + this.memoryStore = memoryStore; + } + + public async IAsyncEnumerable ProcessAsync(RagPipelineContext context, IAsyncEnumerable source, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (context.Embedding.Length == 0) + { + throw new InvalidOperationException("Embedding has not been calculated yet."); + } + + var records = memoryStore.GetNearestMatchesAsync(options.CollectionName, + context.Embedding, + context.Limit, + context.MinRelevanceScore, + options.WithEmbeddings, + cancellationToken); + + await foreach (var (record, relevance) in records.WithCancellation(cancellationToken)) + { + yield return MemoryQueryResult.FromMemoryRecord(record, relevance); + } + } +} diff --git a/text/Squidex.Text/ChatBots/IChatTool.cs b/ai/Squidex.AI/SemanticKernel/MemoryStoreStepOptions.cs similarity index 66% rename from text/Squidex.Text/ChatBots/IChatTool.cs rename to ai/Squidex.AI/SemanticKernel/MemoryStoreStepOptions.cs index 41f1e81..f28260f 100644 --- a/text/Squidex.Text/ChatBots/IChatTool.cs +++ b/ai/Squidex.AI/SemanticKernel/MemoryStoreStepOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Text.ChatBots; +namespace Squidex.AI.SemanticKernel; -public interface IChatTool +public sealed class MemoryStoreStepOptions { - ToolSpec Spec { get; } + public string CollectionName { get; set; } - Task ExecuteAsync(Dictionary arguments, - CancellationToken ct); + public bool WithEmbeddings { get; set; } } diff --git a/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatEntity.cs b/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatEntity.cs new file mode 100644 index 0000000..58534c9 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatEntity.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.AI.SemanticKernel.Mongo; + +public sealed class MongoChatEntity +{ + public string Id { get; set; } + + public string Value { get; set; } + + public DateTime Expires { get; set; } +} diff --git a/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStore.cs b/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStore.cs new file mode 100644 index 0000000..e1fbd31 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStore.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using Squidex.Hosting; + +namespace Squidex.AI.SemanticKernel.Mongo; + +public sealed class MongoChatStore : IChatStore, IInitializable +{ + private readonly IMongoCollection collection; + + public MongoChatStore(IMongoDatabase database, IOptions options) + { + collection = database.GetCollection(options.Value.CollectionName); + } + + public Task InitializeAsync( + CancellationToken ct) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.Expires), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }), + cancellationToken: ct); + } + + public Task ClearAsync(string conversationId, + CancellationToken ct) + { + return collection.DeleteOneAsync(x => x.Id == conversationId, ct); + } + + public async Task GetAsync(string conversationId, + CancellationToken ct) + { + var result = await collection.Find(x => x.Id == conversationId).FirstOrDefaultAsync(ct); + + return result?.Value; + } + + public Task StoreAsync(string conversationId, string value, DateTime expires, + CancellationToken ct) + { + return collection.UpdateOneAsync(x => x.Id == conversationId, + Builders.Update + .SetOnInsert(x => x.Id, conversationId) + .Set(x => x.Value, value) + .Set(x => x.Expires, expires), + new UpdateOptions + { + IsUpsert = true + }, + ct); + } +} diff --git a/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStoreOptions.cs b/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStoreOptions.cs new file mode 100644 index 0000000..39fb6e7 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/Mongo/MongoChatStoreOptions.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Hosting.Configuration; + +namespace Squidex.AI.SemanticKernel.Mongo; + +public sealed class MongoChatStoreOptions : IValidatableOptions +{ + public string CollectionName { get; set; } = "Chat"; + + public IEnumerable Validate() + { + if (string.IsNullOrWhiteSpace(CollectionName)) + { + yield return new ConfigurationError("Value is required.", nameof(CollectionName)); + } + } +} diff --git a/ai/Squidex.AI/SemanticKernel/Mongo/MongoSemanticKernelServiceExtensions.cs b/ai/Squidex.AI/SemanticKernel/Mongo/MongoSemanticKernelServiceExtensions.cs new file mode 100644 index 0000000..ece139f --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/Mongo/MongoSemanticKernelServiceExtensions.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Squidex.AI.SemanticKernel; +using Squidex.AI.SemanticKernel.Mongo; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class MongoSemanticKernelServiceExtensions +{ + public static IKernelBuilder AddMongoChatStore(this IKernelBuilder builder, IConfiguration config, Action? configure = null, + string configPath = "ai:mongoDb") + { + builder.Services.ConfigureAndValidate(config, configPath, configure); + + builder.Services.AddSingletonAs() + .As(); + + return builder; + } +} diff --git a/ai/Squidex.AI/SemanticKernel/OpenAIChatAgent.cs b/ai/Squidex.AI/SemanticKernel/OpenAIChatAgent.cs new file mode 100644 index 0000000..5a0056f --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/OpenAIChatAgent.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using Azure.AI.OpenAI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Squidex.AI.SemanticKernel; + +internal sealed class OpenAIChatAgent : IChatAgent +{ + private readonly IChatStore store; + private readonly TimeProvider timeProvider; + private readonly Kernel kernel; + private readonly OpenAIChatBotOptions options; + + public bool IsConfigured => kernel.Services.GetService() != null; + + public OpenAIChatAgent(Kernel kernel, IChatStore store, IOptions options, + TimeProvider timeProvider) + { + this.kernel = kernel; + this.store = store; + this.timeProvider = timeProvider; + this.options = options.Value; + } + + public async Task PromptAsync(string conversationId, string prompt, + CancellationToken ct = default) + { + if (kernel == null) + { + return ChatBotResponse.Failed("Not configured."); + } + + var chatCompletionService = kernel.GetRequiredService(); + + var openAIPromptExecutionSettings = new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, + TopP = 1, + Temperature = options.Temperature ?? 1 + }; + + var history = await LoadHistoryAsync(conversationId, ct); + + if (!string.IsNullOrWhiteSpace(prompt)) + { + history.AddUserMessage(prompt); + } + + var result = + await chatCompletionService.GetChatMessageContentsAsync( + history, + openAIPromptExecutionSettings, + kernel, + ct); + + history.AddRange(result); + await StoreHistoryAsync(history, conversationId, ct); + + var content = result[0].Content ?? + throw new ChatException($"Chat does not return a result for ID '{conversationId}'."); + + return ChatBotResponse.Success(content) with + { + EstimatedCostsInEUR = CalculateCosts(result) + }; + } + + private decimal CalculateCosts(IReadOnlyList result) + { + var costs = 0m; + + if (result[0].Metadata?.TryGetValue("Usage", out var m) == true && m is CompletionsUsage usage) + { + costs += usage.PromptTokens * options.PricePerInputTokenInEUR; + costs += usage.CompletionTokens * options.PricePerOutputTokenInEUR; + } + + return costs; + } + + private async Task LoadHistoryAsync(string conversationId, + CancellationToken ct) + { + ChatHistory history; + + var stored = await store.GetAsync(conversationId, ct); + if (stored != null) + { + history = JsonSerializer.Deserialize(stored) ?? + throw new ChatException($"Cannot deserialize conversion with ID '{conversationId}'."); + + return history; + } + + history = []; + foreach (var systemMessage in options.SystemMessages ?? []) + { + history.AddSystemMessage(systemMessage); + } + + return history; + } + + private Task StoreHistoryAsync(ChatHistory history, string conversationId, + CancellationToken ct) + { + var expires = timeProvider.GetLocalNow().UtcDateTime + options.ConversationLifetime; + + var json = JsonSerializer.Serialize(history) ?? + throw new ChatException($"Cannot serialize conversion with ID '{conversationId}'."); + + return store.StoreAsync(conversationId, json, expires, ct); + } + + public Task StopConversationAsync(string conversationId, + CancellationToken ct = default) + { + return store.GetAsync(conversationId, ct); + } +} diff --git a/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotOptions.cs b/ai/Squidex.AI/SemanticKernel/OpenAIChatBotOptions.cs similarity index 75% rename from text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotOptions.cs rename to ai/Squidex.AI/SemanticKernel/OpenAIChatBotOptions.cs index 5380ef0..fcb1112 100644 --- a/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotOptions.cs +++ b/ai/Squidex.AI/SemanticKernel/OpenAIChatBotOptions.cs @@ -5,15 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using OpenAI; -using OpenAI.ObjectModels; +namespace Squidex.AI.SemanticKernel; -namespace Squidex.Text.ChatBots.OpenAI; - -public sealed class OpenAIChatBotOptions : OpenAiOptions +public sealed class OpenAIChatBotOptions { - public string Model { get; set; } = Models.Gpt_3_5_Turbo; - public string[]? SystemMessages { get; set; } public int? MaxAnswerTokens { get; set; } @@ -22,9 +17,11 @@ public sealed class OpenAIChatBotOptions : OpenAiOptions public int CharactersPerToken { get; set; } = 5; - public float? Temperature { get; set; } + public double? Temperature { get; set; } public decimal PricePerInputTokenInEUR { get; set; } = 0.003m / 1000; public decimal PricePerOutputTokenInEUR { get; set; } = 0.004m / 1000; + + public TimeSpan ConversationLifetime { get; set; } = TimeSpan.FromDays(3); } diff --git a/ai/Squidex.AI/SemanticKernel/PromptRerankerOptions.cs b/ai/Squidex.AI/SemanticKernel/PromptRerankerOptions.cs new file mode 100644 index 0000000..e690d97 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/PromptRerankerOptions.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.AI.SemanticKernel; + +#pragma warning disable MA0048 // File name must match type name +public delegate string PromptRerankerPrompt(string question, string answer); +#pragma warning restore MA0048 // File name must match type name + +public sealed class PromptRerankerOptions +{ + public PromptRerankerPrompt? Prompt { get; set; } + + internal string GetPrompt(string question, string answer) + { + var result = Prompt?.Invoke(question, answer); + + if (string.IsNullOrEmpty(result)) + { + result = $""" +You are a language model designed to evaluate the responses of this documentation query system. +You will use a rating scale of 0 to 10, 0 being poorest response and 10 being the best. +Responses with "not specified" or "no specific mention" or "rephrase question" or "unclear" or no documents returned or empty response are considered poor responses. +Responses where the question appears to be answered are considered good. +Responses that contain detailed answers are considered the best. +Also, use your own judgement in analyzing if the question asked is actually answered in the response. +Remember that a response that contains a request to “rephrase the question” is usually a non-response. +Please rate the question/response pair entered. +Only respond with the rating. No explanation necessary. Only integers. + +Question: +{question} + +Response: +{answer} +"""; + } + + return result; + } +} diff --git a/ai/Squidex.AI/SemanticKernel/PromptRerankerStep.cs b/ai/Squidex.AI/SemanticKernel/PromptRerankerStep.cs new file mode 100644 index 0000000..b49038a --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/PromptRerankerStep.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Memory; + +namespace Squidex.AI.SemanticKernel; + +internal sealed class PromptRerankerStep : IRagPipelineStep +{ + private readonly PromptRerankerOptions options; + + public PromptRerankerStep(IOptionsFactory optionsFactory, string name) + { + options = optionsFactory.Create(name); + } + + public async IAsyncEnumerable ProcessAsync(RagPipelineContext context, IAsyncEnumerable source, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var completionService = context.Kernel?.GetRequiredService(); + + if (completionService == null) + { + await foreach (var item in source.WithCancellation(cancellationToken)) + { + yield return item; + } + + yield break; + } + + var sourceTexts = new List<(string Text, string Id, int Rank, int Index)>(); + var sourceResults = new List(); + + await foreach (var result in source.WithCancellation(cancellationToken)) + { + var metadata = (JsonObject)JsonNode.Parse(result.Metadata.AdditionalMetadata)!; + + if (metadata.TryGetPropertyValue("text", out var text) && text?.GetValueKind() == JsonValueKind.String) + { + sourceTexts.Add((text.ToJsonString(), result.Metadata.Id, 0, sourceTexts.Count)); + } + + sourceResults.Add(result); + } + + if (sourceTexts.Count == 0 || sourceTexts.Count != sourceResults.Count) + { + foreach (var item in sourceResults) + { + yield return item; + } + + yield break; + } + + var query = context.Query; + + await Parallel.ForEachAsync(sourceTexts, cancellationToken, async (doc, ct) => + { + var result = await completionService.GetChatMessageContentAsync(options.GetPrompt(query, doc.Text), null, context.Kernel, ct); + + var item = result.Items.FirstOrDefault(); + + if (int.TryParse(result.Content, CultureInfo.InvariantCulture, out var ranking)) + { + sourceTexts[doc.Index] = (doc.Text, doc.Id, ranking, doc.Index); + } + }); + + foreach (var doc in sourceTexts) + { + var original = sourceResults.Find(x => x.Metadata.Id == doc.Id)!; + + yield return original; + } + } +} diff --git a/ai/Squidex.AI/SemanticKernel/RagPipeline.cs b/ai/Squidex.AI/SemanticKernel/RagPipeline.cs new file mode 100644 index 0000000..8084770 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/RagPipeline.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.Memory; + +namespace Squidex.AI.SemanticKernel; + +public sealed class RagPipeline : IRagPipeline +{ + private readonly List steps = []; + + public RagPipeline(IOptionsFactory optionsFactory, string name, IServiceProvider serviceProvider) + { + var options = optionsFactory.Create(name); + + foreach (var factory in options.StepFactories) + { + steps.Add(factory(serviceProvider)); + } + + steps.Reverse(); + } + + public IAsyncEnumerable SearchAsync(RagPipelineContext context, + CancellationToken cancellationToken = default) + { + var source = AsyncEnumerable.Empty(); + + foreach (var step in steps) + { + source = step.ProcessAsync(context, source, cancellationToken); + } + + return source; + } +} diff --git a/ai/Squidex.AI/SemanticKernel/RagPipelineBuilder.cs b/ai/Squidex.AI/SemanticKernel/RagPipelineBuilder.cs new file mode 100644 index 0000000..6ab10fa --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/RagPipelineBuilder.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.AI.SemanticKernel; + +public sealed class RagPipelineBuilder(IServiceCollection services, string name) +{ + public IServiceCollection Services { get; } = services; + + public string Name { get; } = name; + + public RagPipelineBuilder AddStep(Func factory) + { + Services.Configure(Name, options => + { + options.StepFactories.Add(factory); + }); + + return this; + } +} diff --git a/ai/Squidex.AI/SemanticKernel/RagPipelineContext.cs b/ai/Squidex.AI/SemanticKernel/RagPipelineContext.cs new file mode 100644 index 0000000..7aa0593 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/RagPipelineContext.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.SemanticKernel; + +namespace Squidex.AI.SemanticKernel; + +public sealed class RagPipelineContext +{ + required public string Query { get; set; } + + public int Limit { get; set; } = 10; + + public float MinRelevanceScore { get; set; } + + public ReadOnlyMemory Embedding { get; set; } + + public Kernel? Kernel { get; set; } +} \ No newline at end of file diff --git a/ai/Squidex.AI/SemanticKernel/RagPipelineOptions.cs b/ai/Squidex.AI/SemanticKernel/RagPipelineOptions.cs new file mode 100644 index 0000000..f4006e4 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/RagPipelineOptions.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.AI.SemanticKernel; + +public sealed class RagPipelineOptions +{ + public List> StepFactories { get; } = []; +} diff --git a/ai/Squidex.AI/SemanticKernel/RagServiceCollectionExtensions.cs b/ai/Squidex.AI/SemanticKernel/RagServiceCollectionExtensions.cs new file mode 100644 index 0000000..43d7450 --- /dev/null +++ b/ai/Squidex.AI/SemanticKernel/RagServiceCollectionExtensions.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Memory; +using Squidex.AI.SemanticKernel; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class RagServiceCollectionExtensions +{ + public static RagPipelineBuilder AddRagPipeline(this IKernelBuilder builder, string name) + { + builder.Services.AddKeyedSingleton(name, (c, n) => ActivatorUtilities.CreateInstance(c, n!.ToString()!)); + + return new RagPipelineBuilder(builder.Services, name); + } + + public static RagPipelineBuilder AddCalculateEmbeddings(this RagPipelineBuilder builder, ITextEmbeddingGenerationService embeddingGenerationService) + { + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, embeddingGenerationService)); + } + + public static RagPipelineBuilder AddCalculateEmbeddings(this RagPipelineBuilder builder, object? serviceId = null) + { + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, c.GetRequiredKeyedService(serviceId))); + } + + public static RagPipelineBuilder AddCalculateEmbeddings(this RagPipelineBuilder builder, Func factory) + { + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, factory(c))); + } + + public static RagPipelineBuilder AddSearchInMemoryStore(this RagPipelineBuilder builder, IMemoryStore memoryStore, string collectionName, Action? configure = null) + { + builder.Configure(collectionName, configure); + + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, builder.Name, memoryStore)); + } + + public static RagPipelineBuilder AddSearchInMemoryStore(this RagPipelineBuilder builder, string collectionName, object? serviceId = null, Action? configure = null) + { + builder.Configure(collectionName, configure); + + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, builder.Name, c.GetRequiredKeyedService(serviceId))); + } + + public static RagPipelineBuilder AddSearchInMemoryStore(this RagPipelineBuilder builder, Func factory, string collectionName, Action? configure = null) + { + builder.Configure(collectionName, configure); + + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, builder.Name, factory(c))); + } + + public static RagPipelineBuilder AddCohereRerank(this RagPipelineBuilder builder, string apiKey, Action? configure = null) + { + builder.Configure(apiKey, configure); + + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, builder.Name, apiKey)); + } + + public static RagPipelineBuilder AddPromptReranker(this RagPipelineBuilder builder, Action? configure = null) + { + builder.Configure(configure); + + return builder.AddStep(c => + ActivatorUtilities.CreateInstance(c, builder.Name)); + } + + private static void Configure(this RagPipelineBuilder builder, string collectionName, Action? configure) + { + builder.Services.Configure(builder.Name, options => + { + options.CollectionName = collectionName; + configure?.Invoke(options); + }); + } + + private static void Configure(this RagPipelineBuilder builder, string apiKey, Action? configure) + { + builder.Services.Configure(builder.Name, options => + { + options.ApiKey = apiKey; + configure?.Invoke(options); + }); + } + + private static void Configure(this RagPipelineBuilder builder, Action? configure) + { + builder.Services.Configure(builder.Name, options => + { + configure?.Invoke(options); + }); + } +} diff --git a/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotServiceExtensions.cs b/ai/Squidex.AI/SemanticKernel/SementicKernelServiceExtensions.cs similarity index 74% rename from text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotServiceExtensions.cs rename to ai/Squidex.AI/SemanticKernel/SementicKernelServiceExtensions.cs index 138ff0b..de84439 100644 --- a/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatBotServiceExtensions.cs +++ b/ai/Squidex.AI/SemanticKernel/SementicKernelServiceExtensions.cs @@ -7,18 +7,26 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; -using Squidex.Text.ChatBots; -using Squidex.Text.ChatBots.OpenAI; +using Microsoft.SemanticKernel; +using Squidex.AI; +using Squidex.AI.SemanticKernel; namespace Microsoft.Extensions.DependencyInjection; -public static class OpenAIChatBotServiceExtensions +public static class SementicKernelServiceExtensions { + public static IKernelBuilder AddTool(this IKernelBuilder builder) + { + builder.Plugins.AddFromType(); + return builder; + } + public static IServiceCollection AddOpenAIChatAgent(this IServiceCollection services, IConfiguration config, Action? configure = null, string configPath = "chatbot:openai") { services.Configure(config, configPath, configure); + services.TryAddSingleton(TimeProvider.System); services.TryAddSingleton(); services.AddSingletonAs() .As().AsSelf(); diff --git a/ai/Squidex.AI/Squidex.AI.csproj b/ai/Squidex.AI/Squidex.AI.csproj new file mode 100644 index 0000000..62fdd8f --- /dev/null +++ b/ai/Squidex.AI/Squidex.AI.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + Latest + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/ai/Squidex.AI/logo-squared.png b/ai/Squidex.AI/logo-squared.png new file mode 100644 index 0000000000000000000000000000000000000000..3cbc19038a372bfa3b1094aedc915af1fe54b6ce GIT binary patch literal 19430 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 literal 0 HcmV?d00001 diff --git a/assets/Benchmarks/Benchmarks.csproj b/assets/Benchmarks/Benchmarks.csproj index cf0176d..7913b7e 100644 --- a/assets/Benchmarks/Benchmarks.csproj +++ b/assets/Benchmarks/Benchmarks.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.Azure/Squidex.Assets.Azure.csproj b/assets/Squidex.Assets.Azure/Squidex.Assets.Azure.csproj index 78d308f..422c9bd 100644 --- a/assets/Squidex.Assets.Azure/Squidex.Assets.Azure.csproj +++ b/assets/Squidex.Assets.Azure/Squidex.Assets.Azure.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.FTP/Squidex.Assets.FTP.csproj b/assets/Squidex.Assets.FTP/Squidex.Assets.FTP.csproj index 6997de9..da117e5 100644 --- a/assets/Squidex.Assets.FTP/Squidex.Assets.FTP.csproj +++ b/assets/Squidex.Assets.FTP/Squidex.Assets.FTP.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.GoogleCloud/Squidex.Assets.GoogleCloud.csproj b/assets/Squidex.Assets.GoogleCloud/Squidex.Assets.GoogleCloud.csproj index d5d0181..31dff6b 100644 --- a/assets/Squidex.Assets.GoogleCloud/Squidex.Assets.GoogleCloud.csproj +++ b/assets/Squidex.Assets.GoogleCloud/Squidex.Assets.GoogleCloud.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.ImageMagick/Squidex.Assets.ImageMagick.csproj b/assets/Squidex.Assets.ImageMagick/Squidex.Assets.ImageMagick.csproj index ae3aa3a..e5aa67c 100644 --- a/assets/Squidex.Assets.ImageMagick/Squidex.Assets.ImageMagick.csproj +++ b/assets/Squidex.Assets.ImageMagick/Squidex.Assets.ImageMagick.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.ImageSharp/ImageSharpThumbnailGenerator.cs b/assets/Squidex.Assets.ImageSharp/ImageSharpThumbnailGenerator.cs index e62d123..8e953f5 100644 --- a/assets/Squidex.Assets.ImageSharp/ImageSharpThumbnailGenerator.cs +++ b/assets/Squidex.Assets.ImageSharp/ImageSharpThumbnailGenerator.cs @@ -109,7 +109,7 @@ protected override async Task CreateThumbnailCoreAsync(Stream source, string mim if (options.Background != null && Color.TryParse(options.Background, out var color)) { - operation.BackgroundColor(color); + operation.BackgroundColor(color); } else { diff --git a/assets/Squidex.Assets.ImageSharp/Squidex.Assets.ImageSharp.csproj b/assets/Squidex.Assets.ImageSharp/Squidex.Assets.ImageSharp.csproj index 4e2b0f8..697449f 100644 --- a/assets/Squidex.Assets.ImageSharp/Squidex.Assets.ImageSharp.csproj +++ b/assets/Squidex.Assets.ImageSharp/Squidex.Assets.ImageSharp.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -22,7 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/assets/Squidex.Assets.Mongo/Squidex.Assets.Mongo.csproj b/assets/Squidex.Assets.Mongo/Squidex.Assets.Mongo.csproj index 8743be5..c39e088 100644 --- a/assets/Squidex.Assets.Mongo/Squidex.Assets.Mongo.csproj +++ b/assets/Squidex.Assets.Mongo/Squidex.Assets.Mongo.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.ResizeService/Squidex.Assets.ResizeService.csproj b/assets/Squidex.Assets.ResizeService/Squidex.Assets.ResizeService.csproj index 83fcac1..a9d7a8a 100644 --- a/assets/Squidex.Assets.ResizeService/Squidex.Assets.ResizeService.csproj +++ b/assets/Squidex.Assets.ResizeService/Squidex.Assets.ResizeService.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.S3/Squidex.Assets.S3.csproj b/assets/Squidex.Assets.S3/Squidex.Assets.S3.csproj index b6feba4..4af0b16 100644 --- a/assets/Squidex.Assets.S3/Squidex.Assets.S3.csproj +++ b/assets/Squidex.Assets.S3/Squidex.Assets.S3.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.Tests/AssetThumbnailGeneratorTests.cs b/assets/Squidex.Assets.Tests/AssetThumbnailGeneratorTests.cs index e4e4efd..be3f464 100644 --- a/assets/Squidex.Assets.Tests/AssetThumbnailGeneratorTests.cs +++ b/assets/Squidex.Assets.Tests/AssetThumbnailGeneratorTests.cs @@ -17,8 +17,10 @@ public abstract class AssetThumbnailGeneratorTests protected readonly IAssetThumbnailGenerator sut; #pragma warning restore SA1401 // Fields should be private - public static IEnumerable GetConversions() + public static TheoryData GetConversions() { + var result = new TheoryData(); + var allFormats = Enum.GetValues(typeof(ImageFormat)).OfType(); foreach (var source in allFormats) @@ -27,10 +29,12 @@ public static IEnumerable GetConversions() { if (!Equals(target, source)) { - yield return new object[] { target, source }; + result.Add(target, source); } } } + + return result; } protected AssetThumbnailGeneratorTests() @@ -165,7 +169,8 @@ public async Task Should_change_png_quality_and_write_to_target() { await sut.CreateThumbnailAsync(source, mimeType, target, new ResizeOptions { - Quality = 10, Format = ImageFormat.JPEG + Quality = 10, + Format = ImageFormat.JPEG }); Assert.True(target.Length < source.Length); diff --git a/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj b/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj index 4395acf..77dc36a 100644 --- a/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj +++ b/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj @@ -25,16 +25,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/assets/Squidex.Assets.Tests/TusServerFixture.cs b/assets/Squidex.Assets.Tests/TusServerFixture.cs index 761d2a0..85abcb8 100644 --- a/assets/Squidex.Assets.Tests/TusServerFixture.cs +++ b/assets/Squidex.Assets.Tests/TusServerFixture.cs @@ -67,7 +67,6 @@ public TusServerFixture() }); app.UseRouting(); - app.UseEndpoints(builder => { builder.MapControllers(); diff --git a/assets/Squidex.Assets.TusAdapter/Squidex.Assets.TusAdapter.csproj b/assets/Squidex.Assets.TusAdapter/Squidex.Assets.TusAdapter.csproj index f490ab2..5c3899d 100644 --- a/assets/Squidex.Assets.TusAdapter/Squidex.Assets.TusAdapter.csproj +++ b/assets/Squidex.Assets.TusAdapter/Squidex.Assets.TusAdapter.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets.TusClient/Squidex.Assets.TusClient.csproj b/assets/Squidex.Assets.TusClient/Squidex.Assets.TusClient.csproj index 8f25951..3f7a90d 100644 --- a/assets/Squidex.Assets.TusClient/Squidex.Assets.TusClient.csproj +++ b/assets/Squidex.Assets.TusClient/Squidex.Assets.TusClient.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/assets/Squidex.Assets/Remote/RemoteThumbnailGenerator.cs b/assets/Squidex.Assets/Remote/RemoteThumbnailGenerator.cs index 966338d..ef46eb2 100644 --- a/assets/Squidex.Assets/Remote/RemoteThumbnailGenerator.cs +++ b/assets/Squidex.Assets/Remote/RemoteThumbnailGenerator.cs @@ -41,68 +41,62 @@ public override bool IsResizable(string mimeType, ResizeOptions options, [MaybeN protected override async Task ComputeBlurHashCoreAsync(Stream source, string mimeType, BlurOptions options, CancellationToken ct = default) { - using (var httpClient = httpClientFactory.CreateClient("Resize")) + using var httpClient = httpClientFactory.CreateClient("Resize"); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/blur?{BuildQueryString(options)}") { - var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"/blur?{BuildQueryString(options)}") - { - Content = new StreamContent(source) - }; + Content = new StreamContent(source) + }; - requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); - var response = await httpClient.SendAsync(requestMessage, ct); + using var httpResponse = await httpClient.SendAsync(httpRequest, ct); - response.EnsureSuccessStatusCode(); + httpResponse.EnsureSuccessStatusCode(); - var result = await response.Content.ReadAsStringAsync(ct); + var result = await httpResponse.Content.ReadAsStringAsync(ct); - if (string.IsNullOrWhiteSpace(result)) - { - result = null; - } - - return result; + if (string.IsNullOrWhiteSpace(result)) + { + result = null; } + + return result; } protected override async Task CreateThumbnailCoreAsync(Stream source, string mimeType, Stream destination, ResizeOptions options, CancellationToken ct = default) { - using (var httpClient = httpClientFactory.CreateClient("Resize")) + using var httpClient = httpClientFactory.CreateClient("Resize"); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/resize{BuildQueryString(options)}") { - var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"/resize{BuildQueryString(options)}") - { - Content = new StreamContent(source) - }; + Content = new StreamContent(source) + }; - requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); - var response = await httpClient.SendAsync(requestMessage, ct); + using var httpResonse = await httpClient.SendAsync(httpRequest, ct); - response.EnsureSuccessStatusCode(); + httpResonse.EnsureSuccessStatusCode(); - await response.Content.CopyToAsync(destination, ct); - } + await httpResonse.Content.CopyToAsync(destination, ct); } protected override async Task FixCoreAsync(Stream source, string mimeType, Stream destination, CancellationToken ct = default) { - using (var httpClient = httpClientFactory.CreateClient("Resize")) + using var httpClient = httpClientFactory.CreateClient("Resize"); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/orient") { - var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/orient") - { - Content = new StreamContent(source) - }; + Content = new StreamContent(source) + }; - requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); - var response = await httpClient.SendAsync(requestMessage, ct); + var httpResponse = await httpClient.SendAsync(httpRequest, ct); - response.EnsureSuccessStatusCode(); + httpResponse.EnsureSuccessStatusCode(); - await response.Content.CopyToAsync(destination, ct); - } + await httpResponse.Content.CopyToAsync(destination, ct); } protected override Task GetImageInfoCoreAsync(Stream source, string mimeType, diff --git a/assets/Squidex.Assets/Squidex.Assets.csproj b/assets/Squidex.Assets/Squidex.Assets.csproj index 66fe43c..aa17f99 100644 --- a/assets/Squidex.Assets/Squidex.Assets.csproj +++ b/assets/Squidex.Assets/Squidex.Assets.csproj @@ -12,18 +12,18 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/assets/TusTestServer/TusTestServer.csproj b/assets/TusTestServer/TusTestServer.csproj index fab54de..7201d43 100644 --- a/assets/TusTestServer/TusTestServer.csproj +++ b/assets/TusTestServer/TusTestServer.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/caching/Squidex.Caching.Tests/Squidex.Caching.Tests.csproj b/caching/Squidex.Caching.Tests/Squidex.Caching.Tests.csproj index d62f2c6..b496200 100644 --- a/caching/Squidex.Caching.Tests/Squidex.Caching.Tests.csproj +++ b/caching/Squidex.Caching.Tests/Squidex.Caching.Tests.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/caching/Squidex.Caching/Squidex.Caching.csproj b/caching/Squidex.Caching/Squidex.Caching.csproj index 9d23c2f..1481fe7 100644 --- a/caching/Squidex.Caching/Squidex.Caching.csproj +++ b/caching/Squidex.Caching/Squidex.Caching.csproj @@ -12,13 +12,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/hosting/Squidex.Hosting.Abstractions/Squidex.Hosting.Abstractions.csproj b/hosting/Squidex.Hosting.Abstractions/Squidex.Hosting.Abstractions.csproj index 1c2c1b0..502d1ea 100644 --- a/hosting/Squidex.Hosting.Abstractions/Squidex.Hosting.Abstractions.csproj +++ b/hosting/Squidex.Hosting.Abstractions/Squidex.Hosting.Abstractions.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/hosting/Squidex.Hosting.TestRunner/Squidex.Hosting.TestRunner.csproj b/hosting/Squidex.Hosting.TestRunner/Squidex.Hosting.TestRunner.csproj index 9bc1fcc..76c8386 100644 --- a/hosting/Squidex.Hosting.TestRunner/Squidex.Hosting.TestRunner.csproj +++ b/hosting/Squidex.Hosting.TestRunner/Squidex.Hosting.TestRunner.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/hosting/Squidex.Hosting.Tests/Squidex.Hosting.Tests.csproj b/hosting/Squidex.Hosting.Tests/Squidex.Hosting.Tests.csproj index 2e9316e..ceaa70c 100644 --- a/hosting/Squidex.Hosting.Tests/Squidex.Hosting.Tests.csproj +++ b/hosting/Squidex.Hosting.Tests/Squidex.Hosting.Tests.csproj @@ -10,11 +10,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/hosting/Squidex.Hosting/Squidex.Hosting.csproj b/hosting/Squidex.Hosting/Squidex.Hosting.csproj index 1aea666..8af4ed8 100644 --- a/hosting/Squidex.Hosting/Squidex.Hosting.csproj +++ b/hosting/Squidex.Hosting/Squidex.Hosting.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/log/Squidex.Log.Tests/Squidex.Log.Tests.csproj b/log/Squidex.Log.Tests/Squidex.Log.Tests.csproj index d97565d..e2961de 100644 --- a/log/Squidex.Log.Tests/Squidex.Log.Tests.csproj +++ b/log/Squidex.Log.Tests/Squidex.Log.Tests.csproj @@ -10,12 +10,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/log/Squidex.Log/Internal/ConsoleLogProcessor.cs b/log/Squidex.Log/Internal/ConsoleLogProcessor.cs index 2694f8a..c16ec40 100644 --- a/log/Squidex.Log/Internal/ConsoleLogProcessor.cs +++ b/log/Squidex.Log/Internal/ConsoleLogProcessor.cs @@ -8,7 +8,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; namespace Squidex.Log.Internal; @@ -33,7 +32,8 @@ public ConsoleLogProcessor() outputThread = new Thread(ProcessLogQueue) { - IsBackground = true, Name = "Logging" + IsBackground = true, + Name = "Logging" }; outputThread.Start(); diff --git a/log/Squidex.Log/Internal/FileLogProcessor.cs b/log/Squidex.Log/Internal/FileLogProcessor.cs index 86e6a90..5326f94 100644 --- a/log/Squidex.Log/Internal/FileLogProcessor.cs +++ b/log/Squidex.Log/Internal/FileLogProcessor.cs @@ -28,7 +28,8 @@ public FileLogProcessor(string path) outputThread = new Thread(ProcessLogQueue) { - IsBackground = true, Name = "Logging" + IsBackground = true, + Name = "Logging" }; } diff --git a/log/Squidex.Log/Squidex.Log.csproj b/log/Squidex.Log/Squidex.Log.csproj index 1f96109..6ddf4e9 100644 --- a/log/Squidex.Log/Squidex.Log.csproj +++ b/log/Squidex.Log/Squidex.Log.csproj @@ -12,12 +12,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/messaging/Squidex.Messaging.All/Squidex.Messaging.All.csproj b/messaging/Squidex.Messaging.All/Squidex.Messaging.All.csproj index f56a177..784283e 100644 --- a/messaging/Squidex.Messaging.All/Squidex.Messaging.All.csproj +++ b/messaging/Squidex.Messaging.All/Squidex.Messaging.All.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/messaging/Squidex.Messaging.GoogleCloud/Squidex.Messaging.GoogleCloud.csproj b/messaging/Squidex.Messaging.GoogleCloud/Squidex.Messaging.GoogleCloud.csproj index 8799fd9..8f94e19 100644 --- a/messaging/Squidex.Messaging.GoogleCloud/Squidex.Messaging.GoogleCloud.csproj +++ b/messaging/Squidex.Messaging.GoogleCloud/Squidex.Messaging.GoogleCloud.csproj @@ -12,8 +12,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/messaging/Squidex.Messaging.Kafka/Squidex.Messaging.Kafka.csproj b/messaging/Squidex.Messaging.Kafka/Squidex.Messaging.Kafka.csproj index 8322c3f..3398c8e 100644 --- a/messaging/Squidex.Messaging.Kafka/Squidex.Messaging.Kafka.csproj +++ b/messaging/Squidex.Messaging.Kafka/Squidex.Messaging.Kafka.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/messaging/Squidex.Messaging.Mongo/Squidex.Messaging.Mongo.csproj b/messaging/Squidex.Messaging.Mongo/Squidex.Messaging.Mongo.csproj index e9bd531..46e2f70 100644 --- a/messaging/Squidex.Messaging.Mongo/Squidex.Messaging.Mongo.csproj +++ b/messaging/Squidex.Messaging.Mongo/Squidex.Messaging.Mongo.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/messaging/Squidex.Messaging.RabbitMq/RabbitMqSubscription.cs b/messaging/Squidex.Messaging.RabbitMq/RabbitMqSubscription.cs index bd4003c..34aa64c 100644 --- a/messaging/Squidex.Messaging.RabbitMq/RabbitMqSubscription.cs +++ b/messaging/Squidex.Messaging.RabbitMq/RabbitMqSubscription.cs @@ -31,24 +31,31 @@ public RabbitMqSubscription(string queueName, RabbitMqOwner factory, eventConsumer.Received += async (_, @event) => { - var headers = new TransportHeaders(); - - foreach (var (key, value) in @event.BasicProperties.Headers) + try { - if (value is byte[] bytes) - { - headers[key] = Encoding.UTF8.GetString(bytes); - } - else if (value is string text) + var headers = new TransportHeaders(); + + foreach (var (key, value) in @event.BasicProperties.Headers) { - headers[key] = text; + if (value is byte[] bytes) + { + headers[key] = Encoding.UTF8.GetString(bytes); + } + else if (value is string text) + { + headers[key] = text; + } } - } - var transportMessage = new TransportMessage(@event.Body.ToArray(), @event.RoutingKey, headers); - var transportResult = new TransportResult(transportMessage, @event.DeliveryTag); + var transportMessage = new TransportMessage(@event.Body.ToArray(), @event.RoutingKey, headers); + var transportResult = new TransportResult(transportMessage, @event.DeliveryTag); - await callback(transportResult, this, stopToken.Token); + await callback(transportResult, this, stopToken.Token); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to handle message from queue {queue}.", queueName); + } }; consumerTag = model.BasicConsume(queueName, false, eventConsumer); diff --git a/messaging/Squidex.Messaging.RabbitMq/Squidex.Messaging.RabbitMq.csproj b/messaging/Squidex.Messaging.RabbitMq/Squidex.Messaging.RabbitMq.csproj index 86e7318..f30f660 100644 --- a/messaging/Squidex.Messaging.RabbitMq/Squidex.Messaging.RabbitMq.csproj +++ b/messaging/Squidex.Messaging.RabbitMq/Squidex.Messaging.RabbitMq.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/messaging/Squidex.Messaging.Redis/Squidex.Messaging.Redis.csproj b/messaging/Squidex.Messaging.Redis/Squidex.Messaging.Redis.csproj index e6e0c25..a8e89b7 100644 --- a/messaging/Squidex.Messaging.Redis/Squidex.Messaging.Redis.csproj +++ b/messaging/Squidex.Messaging.Redis/Squidex.Messaging.Redis.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,7 +21,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/messaging/Squidex.Messaging.Subscriptions/Messages/MessageFactories.cs b/messaging/Squidex.Messaging.Subscriptions/Messages/MessageFactories.cs index aec24f3..f0ac56a 100644 --- a/messaging/Squidex.Messaging.Subscriptions/Messages/MessageFactories.cs +++ b/messaging/Squidex.Messaging.Subscriptions/Messages/MessageFactories.cs @@ -12,8 +12,8 @@ namespace Squidex.Messaging.Subscriptions.Messages; internal static class MessageFactories { - private static readonly ConcurrentDictionary> SubscribeFactories = new (); - private static readonly ConcurrentDictionary, object, PayloadMessageBase>> PayloadFactories = new (); + private static readonly ConcurrentDictionary> SubscribeFactories = []; + private static readonly ConcurrentDictionary, object, PayloadMessageBase>> PayloadFactories = []; private static readonly MethodInfo BuildSubscribeFactoryMethod = typeof(MessageFactories).GetMethod(nameof(BuildSubscribeFactory), BindingFlags.NonPublic | BindingFlags.Static)!; diff --git a/messaging/Squidex.Messaging.Subscriptions/Squidex.Messaging.Subscriptions.csproj b/messaging/Squidex.Messaging.Subscriptions/Squidex.Messaging.Subscriptions.csproj index 548fd98..b6f5f48 100644 --- a/messaging/Squidex.Messaging.Subscriptions/Squidex.Messaging.Subscriptions.csproj +++ b/messaging/Squidex.Messaging.Subscriptions/Squidex.Messaging.Subscriptions.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/messaging/Squidex.Messaging.Tests/Squidex.Messaging.Tests.csproj b/messaging/Squidex.Messaging.Tests/Squidex.Messaging.Tests.csproj index f133bb6..58da5fb 100644 --- a/messaging/Squidex.Messaging.Tests/Squidex.Messaging.Tests.csproj +++ b/messaging/Squidex.Messaging.Tests/Squidex.Messaging.Tests.csproj @@ -10,16 +10,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/messaging/Squidex.Messaging/Implementation/DefaultMessageBus.cs b/messaging/Squidex.Messaging/Implementation/DefaultMessageBus.cs index 74d96f2..dd9e1ff 100644 --- a/messaging/Squidex.Messaging/Implementation/DefaultMessageBus.cs +++ b/messaging/Squidex.Messaging/Implementation/DefaultMessageBus.cs @@ -47,6 +47,6 @@ public Task PublishToChannelAsync(object message, ChannelName channel, string? k { Guard.NotNull(message, nameof(message)); - return internalProducer.ProduceAsync(channel, message, key, ct); + return internalProducer.ProduceAsync(channel, message, key, ct); } } diff --git a/messaging/Squidex.Messaging/Implementation/InMemory/InMemorySubscriptionStore.cs b/messaging/Squidex.Messaging/Implementation/InMemory/InMemorySubscriptionStore.cs index 97eb0f9..13f7053 100644 --- a/messaging/Squidex.Messaging/Implementation/InMemory/InMemorySubscriptionStore.cs +++ b/messaging/Squidex.Messaging/Implementation/InMemory/InMemorySubscriptionStore.cs @@ -14,7 +14,7 @@ namespace Squidex.Messaging.Implementation.InMemory; public class InMemorySubscriptionStore : IMessagingSubscriptionStore { - private readonly ConcurrentDictionary<(string Group, string Key), Subscription> subscriptions = new (); + private readonly ConcurrentDictionary<(string Group, string Key), Subscription> subscriptions = []; private sealed record Subscription(SerializedObject Value, DateTime Expiration); @@ -25,7 +25,7 @@ private sealed record Subscription(SerializedObject Value, DateTime Expiration); foreach (var (key, subscription) in subscriptions.Where(x => x.Key.Group == group)) { - result.Add((key.Key, subscription.Value, subscription.Expiration)); + result.Add((key.Key, subscription.Value, subscription.Expiration)); } return Task.FromResult>(result); diff --git a/messaging/Squidex.Messaging/Squidex.Messaging.csproj b/messaging/Squidex.Messaging/Squidex.Messaging.csproj index 097519c..300c4ec 100644 --- a/messaging/Squidex.Messaging/Squidex.Messaging.csproj +++ b/messaging/Squidex.Messaging/Squidex.Messaging.csproj @@ -12,14 +12,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/text/Squidex.Text.Tests/ChatBots/OpenAIChatAgentTests.cs b/text/Squidex.Text.Tests/ChatBots/OpenAIChatAgentTests.cs deleted file mode 100644 index 8ae8ed0..0000000 --- a/text/Squidex.Text.Tests/ChatBots/OpenAIChatAgentTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Squidex.Text.ChatBots; - -public class OpenAIChatAgentTests -{ - private readonly IChatAgent sut; - - public OpenAIChatAgentTests() - { - var services = - new ServiceCollection() - .AddSingleton() - .AddOpenAIChatAgent(TestHelpers.Configuration, options => - { - options.SystemMessages = - [ - "You are a fiendly agent.", - "Say hello to the user." - ]; - options.Temperature = 0; - }) - .BuildServiceProvider(); - - sut = services.GetRequiredService(); - } - - public class MathTool : IChatTool - { - public ToolSpec Spec { get; } = - new ToolSpec("calculator", "Adds two numbers.") - { - Arguments = - [ - new ToolNumberArgumentSpec("a", "The first number") - { - IsRequired = true, - }, - new ToolNumberArgumentSpec("b", "The second number") - { - IsRequired = true, - }, - ] - }; - - public Task ExecuteAsync(Dictionary arguments, CancellationToken ct) - { - var a = (ToolNumberValue)arguments["a"]; - var b = (ToolNumberValue)arguments["b"]; - - return Task.FromResult($"{a.Value + b.Value + 10}"); - } - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void Should_not_be_configure_if_api_key_is_not_defined(string? apiKey) - { - var sut2 = - new ServiceCollection() - .AddOpenAIChatAgent(TestHelpers.Configuration, options => options.ApiKey = apiKey!) - .BuildServiceProvider() - .GetRequiredService(); - - Assert.False(sut2.IsConfigured); - } - - [Fact] - public void Shouldbe_configure_if_api_key_is_defined() - { - var sut2 = - new ServiceCollection() - .AddOpenAIChatAgent(TestHelpers.Configuration, options => options.ApiKey = "My Api Key") - .BuildServiceProvider() - .GetRequiredService(); - - Assert.True(sut2.IsConfigured); - } - - [Fact] - [Trait("Category", "Dependencies")] - public async Task Should_ask_questions() - { - var conversation = Guid.NewGuid().ToString(); - - try - { - var message1 = await sut.PromptAsync(conversation, string.Empty); - - AssertMessage("Hello! How can I assist you today?", message1); - - var message2 = await sut.PromptAsync(conversation, "Provide an interesting article about Paris in 5 words."); - - AssertMessage("Paris: City of Love and Lights.", message2); - } - finally - { - await sut.StopConversationAsync(conversation); - } - } - - [Fact] - [Trait("Category", "Dependencies")] - public async Task Should_ask_question_with_tool() - { - var conversation = Guid.NewGuid().ToString(); - try - { - var message1 = await sut.PromptAsync(conversation, string.Empty); - - AssertMessage("Hello! How can I assist you today?", message1); - - var message2 = await sut.PromptAsync(conversation, "What is 10 plus 42 using the tool."); - - AssertMessage("The sum of 10 and 42 is 62.", message2); - } - finally - { - await sut.StopConversationAsync(conversation); - } - } - - [Fact] - [Trait("Category", "Dependencies")] - public async Task Should_ask_multiple_question_with_tools() - { - var conversation = Guid.NewGuid().ToString(); - try - { - var message1 = await sut.PromptAsync(conversation, string.Empty); - - AssertMessage("Hello! How can I assist you today?", message1); - - var message2 = await sut.PromptAsync(conversation, "What is 10 plus 42 and 4 + 8 using the tool."); - - AssertMessage("The sum of 10 plus 42 is 62, and the sum of 4 plus 8 is 22.", message2); - } - finally - { - await sut.StopConversationAsync(conversation); - } - } - - private static void AssertMessage(string text, ChatBotResponse message) - { - Assert.True(message.EstimatedCostsInEUR is > 0 and < 1); - Assert.Equal(text, message.Text); - } -} diff --git a/text/Squidex.Text.Tests/ChatBots/OpenApiFunctionParser.cs b/text/Squidex.Text.Tests/ChatBots/OpenApiFunctionParser.cs deleted file mode 100644 index 217f6d2..0000000 --- a/text/Squidex.Text.Tests/ChatBots/OpenApiFunctionParser.cs +++ /dev/null @@ -1,199 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FluentAssertions; -using OpenAI.ObjectModels.RequestModels; -using Squidex.Text.ChatBots.OpenAI; -using Xunit; - -namespace Squidex.Text.ChatBots; - -public class OpenApiFunctionParser -{ - [Fact] - public void Should_throw_if_required_value_is_null() - { - var spec = new ToolStringArgumentSpec("string", "My String") - { - IsRequired = true - }; - - var call = new FunctionCall - { - Arguments = "{ \"string\": null }" - }; - - Assert.Throws(() => call.ParseArguments(BuildSpec(spec))); - } - - [Fact] - public void Should_throw_if_required_value_is_not_found() - { - var spec = new ToolStringArgumentSpec("string", "My String") - { - IsRequired = true - }; - - var call = new FunctionCall - { - Arguments = "{ \"string\": null }" - }; - - Assert.Throws(() => call.ParseArguments(BuildSpec(spec))); - } - - [Fact] - public void Should_throw_if_type_does_not_match() - { - var spec = new ToolStringArgumentSpec("string", "My String") - { - IsRequired = true - }; - - var call = new FunctionCall - { - Arguments = "{ \"string\": 42 }" - }; - - Assert.Throws(() => call.ParseArguments(BuildSpec(spec))); - } - - [Fact] - public void Should_parse_null() - { - var spec = new ToolStringArgumentSpec("string", "My String"); - - var call = new FunctionCall - { - Arguments = "{ \"string\": null }" - }; - - var parsed = call.ParseArguments(BuildSpec(spec)); - - parsed.Should().BeEquivalentTo(new Dictionary - { - ["string"] = new ToolNullValue() - }); - } - - [Fact] - public void Should_parse_string() - { - var spec = new ToolStringArgumentSpec("string", "My String"); - - var call = new FunctionCall - { - Arguments = "{ \"string\": \"Hello\" }" - }; - - var parsed = call.ParseArguments(BuildSpec(spec)); - - parsed.Should().BeEquivalentTo(new Dictionary - { - ["string"] = new ToolStringValue("Hello") - }); - } - - [Fact] - public void Should_parse_true() - { - var spec = new ToolBooleanArgumentSpec("boolean", "My Boolean"); - - var call = new FunctionCall - { - Arguments = "{ \"boolean\": true }" - }; - - var parsed = call.ParseArguments(BuildSpec(spec)); - - parsed.Should().BeEquivalentTo(new Dictionary - { - ["boolean"] = new ToolBooleanValue(true) - }); - } - - [Fact] - public void Should_parse_false() - { - var spec = new ToolBooleanArgumentSpec("boolean", "My Boolean"); - - var call = new FunctionCall - { - Arguments = "{ \"boolean\": false }" - }; - - var parsed = call.ParseArguments(BuildSpec(spec)); - - parsed.Should().BeEquivalentTo(new Dictionary - { - ["boolean"] = new ToolBooleanValue(false) - }); - } - - [Fact] - public void Should_parse_number() - { - var spec = new ToolNumberArgumentSpec("number", "My Number"); - - var call = new FunctionCall - { - Arguments = "{ \"number\": 42 }" - }; - - var parsed = call.ParseArguments(BuildSpec(spec)); - - parsed.Should().BeEquivalentTo(new Dictionary - { - ["number"] = new ToolNumberValue(42) - }); - } - - [Fact] - public void Should_parse_enum() - { - var spec = new ToolEnumArgumentSpec("enum", "My Enum") - { - Values = ["A", "B"] - }; - - var call = new FunctionCall - { - Arguments = "{ \"enum\": \"A\" }" - }; - - var parsed = call.ParseArguments(BuildSpec(spec)); - - parsed.Should().BeEquivalentTo(new Dictionary - { - ["enum"] = new ToolStringValue("A") - }); - } - - [Fact] - public void Should_throw_if_enum_has_invalid_value() - { - var spec = new ToolEnumArgumentSpec("enum", "My Enum") - { - Values = ["A", "B"] - }; - - var call = new FunctionCall - { - Arguments = "{ \"enum\": \"C\" }" - }; - - Assert.Throws(() => call.ParseArguments(BuildSpec(spec))); - } - - private static ToolSpec BuildSpec(ToolArgumentSpec spec) - { - return new ToolSpec("function", "My Function") - { - Arguments = [spec] - }; - } -} diff --git a/text/Squidex.Text.Tests/RichText/Json/JsonNode.cs b/text/Squidex.Text.Tests/RichText/Json/JsonNode.cs index 974af3f..58120c9 100644 --- a/text/Squidex.Text.Tests/RichText/Json/JsonNode.cs +++ b/text/Squidex.Text.Tests/RichText/Json/JsonNode.cs @@ -9,7 +9,7 @@ namespace Squidex.RichText.Json; -internal class JsonNode : INode +internal sealed class JsonNode : INode { private readonly JsonMark mark = new JsonMark(); private State currentState; diff --git a/text/Squidex.Text.Tests/Squidex.Text.Tests.csproj b/text/Squidex.Text.Tests/Squidex.Text.Tests.csproj index 453d722..dfb4ff1 100644 --- a/text/Squidex.Text.Tests/Squidex.Text.Tests.csproj +++ b/text/Squidex.Text.Tests/Squidex.Text.Tests.csproj @@ -11,19 +11,20 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/text/Squidex.Text/ChatBots/OpenAI/Helper.cs b/text/Squidex.Text/ChatBots/OpenAI/Helper.cs deleted file mode 100644 index b09adc0..0000000 --- a/text/Squidex.Text/ChatBots/OpenAI/Helper.cs +++ /dev/null @@ -1,157 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Text.Json; -using System.Text.Json.Nodes; -using OpenAI.Builders; -using OpenAI.ObjectModels.RequestModels; -using OpenAI.ObjectModels.SharedModels; - -namespace Squidex.Text.ChatBots.OpenAI; - -internal static class Helper -{ - public static ToolDefinition ToToolDefinition(this ToolSpec spec) - { - var builder = new FunctionDefinitionBuilder(spec.Name, spec.Description); - - foreach (var argument in spec.Arguments) - { - switch (argument) - { - case ToolStringArgumentSpec: - builder.AddParameter(argument.Name, - PropertyDefinition.DefineString(argument.Description), - argument.IsRequired); - break; - case ToolNumberArgumentSpec: - builder.AddParameter(argument.Name, - PropertyDefinition.DefineNumber(argument.Description), - argument.IsRequired); - break; - case ToolEnumArgumentSpec enumArg: - builder.AddParameter(argument.Name, - PropertyDefinition.DefineEnum(enumArg.Values.ToList(), argument.Description), - argument.IsRequired); - break; - } - } - - return ToolDefinition.DefineFunction(builder.Build()); - } - - public static bool AppendMessage(this ChatCompletionCreateRequest request, ChatMessage message, int maxContextLength, int charactersPerToken) - { - if (string.IsNullOrWhiteSpace(message.Content)) - { - return true; - } - - var newTokens = CalculateTokens(message); - if (newTokens > maxContextLength) - { - return false; - } - - request.Messages.Add(message); - - var totalTokens = request.Messages.Sum(CalculateTokens); - - while (totalTokens > maxContextLength && request.Messages.Count > 0) - { - var first = request.Messages.RemoveAtAndReturn(0); - - totalTokens -= CalculateTokens(first); - } - - return true; - - int CalculateTokens(ChatMessage message) - { - if (message.Content is not { Length: > 0 }) - { - return 0; - } - - return (int)Math.Floor((float)message.Content.Length / charactersPerToken); - } - } - - public static T RemoveAtAndReturn(this IList source, int index) - { - var item = source[index]; - - source.RemoveAt(index); - return item; - } - - public static Dictionary ParseArguments(this FunctionCall call, ToolSpec spec) - { - var result = new Dictionary(); - - if (string.IsNullOrWhiteSpace(call.Arguments)) - { - return result; - } - - if (JsonNode.Parse(call.Arguments) is not JsonObject values) - { - throw new ChatException("Argument is not an object."); - } - - foreach (var argument in spec.Arguments) - { - values.TryGetPropertyValue(argument.Name, out var value); - - if (value == null || value.GetValueKind() == JsonValueKind.Null) - { - if (argument.IsRequired) - { - throw new ChatException($"Parameter '{argument.Name}' is not part of the arguments, but required."); - } - - result[argument.Name] = ToolValue.Null; - continue; - } - - var kind = value.GetValueKind(); - - ToolValue parsed; - switch (argument) - { - case ToolBooleanArgumentSpec _ when kind == JsonValueKind.True: - parsed = new ToolBooleanValue(true); - break; - case ToolBooleanArgumentSpec _ when kind == JsonValueKind.False: - parsed = new ToolBooleanValue(false); - break; - case ToolNumberArgumentSpec _ when kind == JsonValueKind.Number: - parsed = new ToolNumberValue((double)value.AsValue()!); - break; - case ToolStringArgumentSpec _ when kind == JsonValueKind.String: - parsed = new ToolStringValue((string)value.AsValue()!); - break; - case ToolEnumArgumentSpec stringArg when kind == JsonValueKind.String: - var stringValue = (string)value.AsValue()!; - - if (!stringArg.Values.Contains(stringValue)) - { - throw new ChatException($"Unexpected value '{stringValue}' for argument '{argument.Name}'."); - } - - parsed = new ToolStringValue(stringValue); - break; - default: - throw new ChatException($"Unexpected kind '{kind}' for argument '{argument.Name}'. Expected string or number."); - } - - result[argument.Name] = parsed; - } - - return result; - } -} diff --git a/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatAgent.cs b/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatAgent.cs deleted file mode 100644 index afa0a42..0000000 --- a/text/Squidex.Text/ChatBots/OpenAI/OpenAIChatAgent.cs +++ /dev/null @@ -1,188 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Text.Json; -using Microsoft.Extensions.Options; -using OpenAI.Managers; -using OpenAI.ObjectModels.RequestModels; - -namespace Squidex.Text.ChatBots.OpenAI; - -public sealed class OpenAIChatAgent : IChatAgent -{ - private const int MaxToolRuns = 5; - private readonly IChatStore chatStore; - private readonly Dictionary chatTools; - private readonly OpenAIChatBotOptions options; - private readonly List tools = []; - private OpenAIService? service; - - public bool IsConfigured { get; } - - public OpenAIChatAgent(IOptions options, IChatStore chatStore, - IEnumerable chatTools) - { - this.options = options.Value; - this.chatStore = chatStore; - this.chatTools = chatTools.ToDictionary(x => x.Spec.Name); - - IsConfigured = !string.IsNullOrWhiteSpace(options.Value.ApiKey); - - if (!IsConfigured) - { - return; - } - - tools.AddRange(chatTools.Select(x => x.Spec.ToToolDefinition())); - } - - public Task StopConversationAsync(string conversationId, - CancellationToken ct = default) - { - return chatStore.ClearAsync(conversationId, ct); - } - - public async Task PromptAsync(string conversationId, string prompt, - CancellationToken ct = default) - { - if (!IsConfigured) - { - return ChatBotResponse.Failed("Agent is not enabled."); - } - - var request = await GetOrCreateConversationAsync(conversationId, ct); - - if (!request.AppendMessage(ChatMessage.FromUser(prompt), options.MaxContextLength, options.CharactersPerToken)) - { - return ChatBotResponse.Failed("Input is too large for the agent."); - } - - return await RequestAsync(conversationId, request, ct); - } - - private async Task RequestAsync(string conversationId, ChatCompletionCreateRequest request, - CancellationToken ct) - { - service ??= new OpenAIService(options); - - async Task<(ChatBotResponse, int)> RequestCoreAsync() - { - var numTokens = 0; - for (var run = 0; run < MaxToolRuns; run++) - { - var response = await service.ChatCompletion.CreateCompletion(request, cancellationToken: ct); - - numTokens += response.Usage?.PromptTokens ?? 0; - numTokens += response.Usage?.CompletionTokens ?? 0; - - if (response.Error != null) - { - return (ChatBotResponse.Failed(response.Error.Message ?? "Unknown error."), numTokens); - } - - var choice = response.Choices[0].Message; - - request.Messages.Add(choice); - - if (choice.ToolCalls is not { Count: > 0 }) - { - return (ChatBotResponse.Success(choice.Content!), numTokens); - } - - var validCalls = new List<(IChatTool Tool, int Index, string Id, FunctionCall Call)>(); - - var i = 0; - foreach (var call in choice.ToolCalls) - { - if (string.IsNullOrWhiteSpace(call.FunctionCall?.Name)) - { - return (ChatBotResponse.Failed("Tool has no function name."), numTokens); - } - - if (!chatTools.TryGetValue(call.FunctionCall.Name, out var tool)) - { - return (ChatBotResponse.Failed($"Tool has unknown function name '{call.FunctionCall.Name}'."), numTokens); - } - - validCalls.Add((tool, i++, call.Id, call.FunctionCall)); - } - - var results = new ChatMessage[validCalls.Count]; - - // Run all the tools in parallel, because they could take long time potentially. - await Parallel.ForEachAsync(validCalls, ct, async (job, ct) => - { - var result = await job.Tool.ExecuteAsync(job.Call.ParseArguments(job.Tool.Spec), ct); - - results[job.Index] = ChatMessage.FromTool(result, job.Id); - }); - - foreach (var result in results) - { - request.Messages.Add(result); - } - } - - return (ChatBotResponse.Failed("Exceeded max tool runs."), numTokens); - } - - var (result, numTokens) = await RequestCoreAsync(); - - var conversation = new Conversation - { - Messages = request.Messages.ToList(), - }; - - await chatStore.StoreAsync(conversationId, JsonSerializer.Serialize(conversation), default); - - return result with - { - EstimatedCostsInEUR = numTokens * options.PricePerInputTokenInEUR - }; - } - - private async Task GetOrCreateConversationAsync(string conversationId, - CancellationToken ct) - { - var request = new ChatCompletionCreateRequest - { - MaxTokens = options.MaxAnswerTokens, - Messages = [], - Model = options.Model, - Temperature = options.Temperature, - Tools = tools.Count > 0 ? tools : null, - N = 1 - }; - - var stored = await chatStore.GetAsync(conversationId, ct); - - if (stored != null) - { - var conversation = JsonSerializer.Deserialize(stored) ?? - throw new ChatException($"Cannot deserialize conversion with ID '{conversationId}'."); - - foreach (var message in conversation.Messages) - { - request.Messages.Add(message); - } - } - else if (options.SystemMessages != null) - { - foreach (var systemMessage in options.SystemMessages) - { - request.Messages.Add(ChatMessage.FromSystem(systemMessage)); - } - } - - return request; - } - - private sealed class Conversation - { - public List Messages { get; set; } = []; - } -} diff --git a/text/Squidex.Text/ChatBots/ToolSpec.cs b/text/Squidex.Text/ChatBots/ToolSpec.cs deleted file mode 100644 index 64bcc77..0000000 --- a/text/Squidex.Text/ChatBots/ToolSpec.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#pragma warning disable MA0048 // File name must match type name -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter - -namespace Squidex.Text.ChatBots; - -public sealed record ToolSpec(string Name, string Description) -{ - public ToolArgumentSpec[] Arguments { get; init; } = []; -} - -public abstract record ToolArgumentSpec(string Name, string Description) -{ - public bool IsRequired { get; init; } -} - -public sealed record ToolBooleanArgumentSpec(string Name, string Description) : ToolArgumentSpec(Name, Description) -{ -} - -public sealed record ToolNumberArgumentSpec(string Name, string Description) : ToolArgumentSpec(Name, Description) -{ -} - -public sealed record ToolStringArgumentSpec(string Name, string Description) : ToolArgumentSpec(Name, Description) -{ -} - -public sealed record ToolEnumArgumentSpec(string Name, string Description) : ToolArgumentSpec(Name, Description) -{ - required public string[] Values { get; set; } -} diff --git a/text/Squidex.Text/ChatBots/ToolValue.cs b/text/Squidex.Text/ChatBots/ToolValue.cs deleted file mode 100644 index 6aa0d8b..0000000 --- a/text/Squidex.Text/ChatBots/ToolValue.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#pragma warning disable MA0048 // File name must match type name -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter - -namespace Squidex.Text.ChatBots; - -public abstract record ToolValue -{ - public static readonly ToolNullValue Null = new ToolNullValue(); -} - -public sealed record ToolStringValue(string Value) : ToolValue; - -public sealed record ToolNumberValue(double Value) : ToolValue; - -public sealed record ToolBooleanValue(bool Value) : ToolValue; - -public sealed record ToolNullValue : ToolValue; diff --git a/text/Squidex.Text/ReadOnlyMemoryCharComparer.cs b/text/Squidex.Text/ReadOnlyMemoryCharComparer.cs index 82aee6c..b5c41bb 100644 --- a/text/Squidex.Text/ReadOnlyMemoryCharComparer.cs +++ b/text/Squidex.Text/ReadOnlyMemoryCharComparer.cs @@ -9,7 +9,7 @@ namespace Squidex.Text; -internal class ReadOnlyMemoryCharComparer : IEqualityComparer> +internal sealed class ReadOnlyMemoryCharComparer : IEqualityComparer> { public static readonly ReadOnlyMemoryCharComparer Ordinal = new ReadOnlyMemoryCharComparer(StringComparison.Ordinal); diff --git a/text/Squidex.Text/RichText/MarkdownVisitor.cs b/text/Squidex.Text/RichText/MarkdownVisitor.cs index dd050c3..c37f693 100644 --- a/text/Squidex.Text/RichText/MarkdownVisitor.cs +++ b/text/Squidex.Text/RichText/MarkdownVisitor.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Text; -using Markdig.Renderers.Normalize; using Squidex.Text.RichText.Model; using Squidex.Text.RichText.Writer; diff --git a/text/Squidex.Text/RichText/Model/Node.cs b/text/Squidex.Text/RichText/Model/Node.cs index 477590a..cd48c71 100644 --- a/text/Squidex.Text/RichText/Model/Node.cs +++ b/text/Squidex.Text/RichText/Model/Node.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Text.Json; using System.Text.Json.Serialization; namespace Squidex.Text.RichText.Model; diff --git a/text/Squidex.Text/Squidex.Text.csproj b/text/Squidex.Text/Squidex.Text.csproj index dbb4d37..9df0220 100644 --- a/text/Squidex.Text/Squidex.Text.csproj +++ b/text/Squidex.Text/Squidex.Text.csproj @@ -12,23 +12,23 @@ - - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs index 62e874d..ecff053 100644 --- a/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs +++ b/text/Squidex.Text/Translations/DeepL/DeepLTranslationService.cs @@ -38,7 +38,7 @@ private sealed class TranslationDto public bool IsConfigured { get; } - public DeepLTranslationService(IOptions options, IHttpClientFactory httpClientFactory) + public DeepLTranslationService(IHttpClientFactory httpClientFactory, IOptions options) { this.options = options.Value; this.httpClientFactory = httpClientFactory; @@ -88,47 +88,45 @@ public async Task> TranslateAsync(IEnumerable(jsonString)!; + var jsonString = await httpResponse.Content.ReadAsStringAsync(ct); + var jsonResponse = JsonSerializer.Deserialize(jsonString)!; - var index = 0; - foreach (var translation in jsonResponse.Translations) - { - var estimationSource = textsArray[index]; - var estimatedCosts = estimationSource.Length * options.CostsPerCharacterInEUR; + var index = 0; + foreach (var translation in jsonResponse.Translations) + { + var estimationSource = textsArray[index]; + var estimatedCosts = estimationSource.Length * options.CostsPerCharacterInEUR; - var language = GetSourceLanguage(translation.DetectedSourceLanguage, sourceLanguage); + var language = GetSourceLanguage(translation.DetectedSourceLanguage, sourceLanguage); - results.Add(TranslationResult.Success(translation.Text, language, estimatedCosts)); - index++; - } - } - catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.BadRequest) - { - AddError(TranslationResult.LanguageNotSupported); - } - catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) - { - AddError(TranslationResult.Unauthorized); - } - catch (Exception ex) - { - AddError(TranslationResult.Failed(ex)); + results.Add(TranslationResult.Success(translation.Text, language, estimatedCosts)); + index++; } } + catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.BadRequest) + { + AddError(TranslationResult.LanguageNotSupported); + } + catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + AddError(TranslationResult.Unauthorized); + } + catch (Exception ex) + { + AddError(TranslationResult.Failed(ex)); + } return results; @@ -141,6 +139,15 @@ void AddError(TranslationResult result) } } + private HttpClient CreateClient() + { + var httpClient = httpClientFactory.CreateClient("DeepL"); + + httpClient.DefaultRequestHeaders.Add("Authorization", $"DeepL-Auth-Key {options.AuthKey}"); + + return httpClient; + } + private static string GetSourceLanguage(string language, string? fallback) { var result = language?.ToLowerInvariant(); diff --git a/text/Squidex.Text/Translations/TranslationsServiceExtensions.cs b/text/Squidex.Text/Translations/TranslationsServiceExtensions.cs index 22e332b..9d1d6cf 100644 --- a/text/Squidex.Text/Translations/TranslationsServiceExtensions.cs +++ b/text/Squidex.Text/Translations/TranslationsServiceExtensions.cs @@ -14,6 +14,7 @@ public static class TranslationsServiceExtensions { public static IServiceCollection AddTranslations(this IServiceCollection services) { + services.AddHttpClient(); services.TryAddSingleton(); return services; From 9df9bd543de080962696991c27f12869ab2ba274 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 20 Mar 2024 16:06:04 +0100 Subject: [PATCH 2/2] Create sut. --- ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs | 66 ++++++++++++--------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs b/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs index d59c5df..62491ae 100644 --- a/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs +++ b/ai/Squidex.AI.Tests/OpenAIChatAgentTests.cs @@ -14,30 +14,6 @@ namespace Squidex.AI; public class OpenAIChatAgentTests { - private readonly IChatAgent sut; - - public OpenAIChatAgentTests() - { - var services = - new ServiceCollection() - .AddKernel() - .AddTool() - .AddTool() - .AddOpenAIChatCompletion("gpt-3.5-turbo-0125", TestHelpers.Configuration["chatBot:openai:apiKey"]!).Services - .AddOpenAIChatAgent(TestHelpers.Configuration, options => - { - options.SystemMessages = - [ - "You are a fiendly agent. Always use the result from the tool if you have called one.", - "Say hello to the user." - ]; - options.Temperature = 0; - }) - .BuildServiceProvider(); - - sut = services.GetRequiredService(); - } - public sealed class MathTool { #pragma warning disable @@ -78,34 +54,36 @@ public sealed class WheatherTool [Fact] public void Should_not_be_configured_if_open_ai_is_not_added() { - var sut2 = + var sut = new ServiceCollection() .AddKernel().Services .AddOpenAIChatAgent(TestHelpers.Configuration) .BuildServiceProvider() .GetRequiredService(); - Assert.False(sut2.IsConfigured); + Assert.False(sut.IsConfigured); } [Fact] public void Should_be_configured_if_open_ai_is_added() { - var sut2 = + var sut = new ServiceCollection() .AddKernel() - .AddOpenAIChatCompletion("gpt-3.5-turbo-0125", TestHelpers.Configuration["chatBot:openai:apiKey"]!).Services + .AddOpenAIChatCompletion("gpt-3.5-turbo-0125", "apiKey").Services .AddOpenAIChatAgent(TestHelpers.Configuration) .BuildServiceProvider() .GetRequiredService(); - Assert.True(sut2.IsConfigured); + Assert.True(sut.IsConfigured); } [Fact] [Trait("Category", "Dependencies")] public async Task Should_ask_questions() { + var sut = CreateSut(); + var conversation = Guid.NewGuid().ToString(); try @@ -126,6 +104,8 @@ public async Task Should_ask_questions() [Trait("Category", "Dependencies")] public async Task Should_ask_question_with_tool() { + var sut = CreateSut(); + var conversation = Guid.NewGuid().ToString(); try { @@ -145,6 +125,8 @@ public async Task Should_ask_question_with_tool() [Trait("Category", "Dependencies")] public async Task Should_ask_question_with_tool2() { + var sut = CreateSut(); + var conversation = Guid.NewGuid().ToString(); try { @@ -164,6 +146,8 @@ public async Task Should_ask_question_with_tool2() [Trait("Category", "Dependencies")] public async Task Should_ask_multiple_question_with_tools() { + var sut = CreateSut(); + var conversation = Guid.NewGuid().ToString(); try { @@ -183,6 +167,8 @@ public async Task Should_ask_multiple_question_with_tools() [Trait("Category", "Dependencies")] public async Task Should_ask_multiple_question_with_tools2() { + var sut = CreateSut(); + var conversation = Guid.NewGuid().ToString(); try { @@ -198,6 +184,28 @@ public async Task Should_ask_multiple_question_with_tools2() } } + private static IChatAgent CreateSut() + { + var services = + new ServiceCollection() + .AddKernel() + .AddTool() + .AddTool() + .AddOpenAIChatCompletion("gpt-3.5-turbo-0125", TestHelpers.Configuration["chatBot:openai:apiKey"]!).Services + .AddOpenAIChatAgent(TestHelpers.Configuration, options => + { + options.SystemMessages = + [ + "You are a fiendly agent. Always use the result from the tool if you have called one.", + "Say hello to the user." + ]; + options.Temperature = 0; + }) + .BuildServiceProvider(); + + return services.GetRequiredService(); + } + private static void AssertMessage(string text, ChatBotResponse message) { Assert.True(message.EstimatedCostsInEUR is > 0 and < 1);