diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4aebcb0..28a2b32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ on: workflow_dispatch: env: - BuildConfiguration: ${{ startsWith(github.ref, 'refs/heads/releases') && 'Release' || 'Debug' }} + BuildConfiguration: ${{ (startsWith(github.ref, 'refs/heads/releases') || startsWith(github.ref, 'refs/main')) && 'Release' || 'Debug' }} jobs: CI: @@ -29,6 +29,12 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + + - name: Set Java for SonarCloud + uses: actions/setup-java@v4 + with: + distribution: 'microsoft' + java-version: '21' - name: Using .NET from 'global.json' uses: actions/setup-dotnet@v3 @@ -39,7 +45,7 @@ jobs: run: dotnet restore --configfile NuGet.config --verbosity Minimal - name: Sonar - Install SonarCloud scanners - run: dotnet tool install --global dotnet-sonarscanner --version 5.15.0 + run: dotnet tool install --global dotnet-sonarscanner --version 6.0.0 - name: Sonar - Begin Analyze env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c1b4b..db9a78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,84 @@ Also, any bug fix must start with the prefix �Bug fix:� followed by the desc Previous classification is not required if changes are simple or all belong to the same category. +## [8.1.0] + +### Important + +This version updates the `Semantic Kernel` library from version `1.0.0-beta8` to `1.1.0`, which introduces a lot of breaking changes in the code. + +Sadly, some features from `Semantic Kernel` that we might have been using, are marked as ***experimental*** and produce warnings that do not allow the compilation of the code. To use these features, these warnings must be ignored explicitly per project. The following is a list of these warnings and the affected projects: + + - SKEXP0001: + - `Encamina.Enmarcha.SemanticKernel` + - SKEXP0003: + - `Encamina.Enmarcha.SemanticKernel` + - `Encamina.Enmarcha.SemanticKernel.Abstractions` + - `Encamina.Enmarcha.SemanticKernel.Connectors.Memory` + - `Encamina.Enmarcha.SemanticKernel.Plugins.Memory` + - `Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering` + - SKEXP0011: + - `Encamina.Enmarcha.SemanticKernel.Connectors.Memory` + - SKEXP0026: + - `Encamina.Enmarcha.SemanticKernel.Connectors.Memory` + - SKEXP0051: + - `Encamina.Enmarcha.SemanticKernel.Connectors.Document` + + More information about these warnings is available here: https://github.com/microsoft/semantic-kernel/blob/main/dotnet/docs/EXPERIMENTS.md + +### Braking Changes + +- Replaced class `Completition` for `Completion` in `Encamina.Enmarcha.AI.OpenAI.Abstractions`. It was misspelled. +- Class `SemanticKernelOptions` does not exists anymore. It has been replaced by `AzureOpenAIOptions` from `Encamina.Enmarcha.AI.OpenAI.Abstractions`. +- The following references were updated due to changes in `Semantic Kernel` version `1.0.1`: + - Changed `IKernel` for `Kernel`. + - Changed `ISKFunction` for `KernelFunction` or `KernelPlugin`. + - Changed `SKFunction` for `KernelFunction`. + - Changed `ContextVariables` for `KernelArguments`. + - Changed `kernel.Functions.GetFunction(...)` for `kernel.Plugins[][]`. + - Changed `OpenAIRequestSettings` for `OpenAIPromptExecutionSettings`. +- Removed extension methods for `SKContext` because that class does not exists anymore in `Semantic Kernel`. +- Due to the breaking nature of the new version of `Semantic Kernel`, the following extension methods are not available any more and have been replace by new methods, and it was not possible to marked it as `Obsolete`: + - `GetSemanticFunctionPromptAsync` is replaced by `GetKernelFunctionPromptAsync`. + - `GetSemanticFunctionUsedTokensAsync` is replaced by `GetKernelFunctionUsedTokensAsync`. + - `ImportSemanticPluginsFromAssembly` is replaced by `ImportPromptFunctionsFromAssembly`. +- Extension method `GetSemanticFunctionPromptAsync` is no longer available. It is replaced by `GetKernelFunctionPromptAsync`. +- Extension method `ImportQuestionAnsweringPlugin` has different signature. +- Extension method `ImportQuestionAnsweringPluginWithMemory` has different signature. +- Extension method `ImportChatWithHistoryPluginUsingCosmosDb` has different signature. +- The format of prompt function configuration files `config.json` has been modified. + +### Major Changes + +- Updated `Semantic Kernel` from `1.0.0-beta8` to `1.1.0` (second final version of `Semantic Kernel`). +- Updated `Azure.Core` from version `1.36.0` to `1.37.0`. +- Updated `Azure.AI.OpenAI` from version `1.0.0-beta.6` to `1.0.0-beta.12`. +- Updated `Bogus` from version `34.0.2` to `35.4.0`. +- Updated `Microsoft.AspNetCore.Authentication.JwtBearer` from version `8.0.0` to `8.0.1`. +- Updated `Microsoft.AspNetCore.Authentication.OpenIdConnect` from version `8.0.0` to `8.0.1`. +- Updated `Microsoft.Azure.Cosmos` from version `3.37.0` to `3.37.1`. +- Updated `Microsoft.EntityFrameworkCore` from version `8.0.0` to `8.0.1`. +- Updated `Microsoft.Extensions.Options` from version `8.0.0` to `8.0.1`. +- Updated `SharpToken` from version `1.2.12` to `1.2.14`. +- Updated `xunit` from version `2.6.2` to `2.6.6`. +- Updated `xunit.analyzers` from version `1.6.0` to `1.10.0`. +- Updated `xunit.extensibility.core` from version `2.6.2` to `2.6.6`. +- Updated `xunit.runner.visualstudio` from version `2.5.4` to `2.5.6`. +- Updated `StyleCop.Analyzers` from version `1.2.0-beta.507` to `1.2.0-beta.556`. +- Updated `System.Text.Json` from version `8.0.0` to `8.0.1`. +- Updated version from `8.0.3` to `8.1.0` due to all the major and breaking changes. +- Updated some `README.md` files changing `IKernel` for `Kernel`. +- Updated and added new unit tests to cover the main "happy path" of implementations that use `Semantic Kernel`. + +### Minor Changes + +- Replaced reference `Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer` (version `5.1.0`) for `Asp.Versioning.Mvc.ApiExplorer` (version `8.0.0`) which is the new name and implementation of the ASP.NET versioning libraries. +- Updated prompt function configuration files (`config.json`) to new format. +- Renamed files `IKernelExtensions` to `KernelExtensions.cs`. +- Fixed token counting in `ChatWithHistoryPlugin.cs`. +- Updated sample projects. +- Fixed some typos and grammatical errors. + ## [8.0.3] ### Minor Changes @@ -23,12 +101,12 @@ Previous classification is not required if changes are simple or all belong to t ## [8.0.2] -### **Major Changes** +### Major Changes - In `Encamina.Enmarcha.SemanticKernel.Plugins.Text` Summarize Plugin, a new parameter `locale` has been added to control the output language of the generated summary. [(#34)](https://github.com/Encamina/enmarcha/issues/34) ## [8.0.1] -### **Major Changes** +### Major Changes - In `Encamina.Enmarcha.SemanticKernel.Abstractions.ILengthFunctions`, `GptEncoding` is now cached and reused to improve performance. [(#30)](https://github.com/Encamina/enmarcha/pull/30) ### Minor Changes diff --git a/Directory.Build.props b/Directory.Build.props index 98155f0..6305dda 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ - 8.0.3 + 8.1.0 diff --git a/Directory.Build.targets b/Directory.Build.targets index dc09643..3a1d49f 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Enmarcha.sln b/Enmarcha.sln index 827d3d1..e53360d 100644 --- a/Enmarcha.sln +++ b/Enmarcha.sln @@ -140,6 +140,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.Samples.S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering", "samples\SemanticKernel\Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering\Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering.csproj", "{AA1E5E93-FE02-4395-9260-C7C869F22785}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encamina.Enmarcha.SemanticKernel.Tests", "tst\Encamina.Enmarcha.SemanticKernel.Tests\Encamina.Enmarcha.SemanticKernel.Tests.csproj", "{7B6F4DC4-74E2-4013-8DBA-12B7AAAD5278}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -346,6 +348,10 @@ Global {AA1E5E93-FE02-4395-9260-C7C869F22785}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA1E5E93-FE02-4395-9260-C7C869F22785}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA1E5E93-FE02-4395-9260-C7C869F22785}.Release|Any CPU.Build.0 = Release|Any CPU + {7B6F4DC4-74E2-4013-8DBA-12B7AAAD5278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B6F4DC4-74E2-4013-8DBA-12B7AAAD5278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B6F4DC4-74E2-4013-8DBA-12B7AAAD5278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B6F4DC4-74E2-4013-8DBA-12B7AAAD5278}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -361,6 +367,7 @@ Global {BF6C4DFC-3CB3-4C62-8B86-08C0C1537CBC} = {B9E33951-E387-4A80-A652-A908FCBB34F3} {9E8B3AEE-AC1C-4F46-A8D2-3EF550F64005} = {43252034-27E2-4981-AC2D-EA986B287863} {AA1E5E93-FE02-4395-9260-C7C869F22785} = {43252034-27E2-4981-AC2D-EA986B287863} + {7B6F4DC4-74E2-4013-8DBA-12B7AAAD5278} = {CBD50B5F-AFB8-4DA1-9FD7-17D98EB3ED78} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F30DF47A-541C-4383-BCEB-E4108D06A70E} diff --git a/samples/Data/Encamina.Enmarcha.Samples.Data.EntityFramework/Encamina.Enmarcha.Samples.Data.EntityFramework.csproj b/samples/Data/Encamina.Enmarcha.Samples.Data.EntityFramework/Encamina.Enmarcha.Samples.Data.EntityFramework.csproj index cf89777..c6145e1 100644 --- a/samples/Data/Encamina.Enmarcha.Samples.Data.EntityFramework/Encamina.Enmarcha.Samples.Data.EntityFramework.csproj +++ b/samples/Data/Encamina.Enmarcha.Samples.Data.EntityFramework/Encamina.Enmarcha.Samples.Data.EntityFramework.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering.csproj b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering.csproj index c22796e..2142b61 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering.csproj +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,7 +7,20 @@ enable + + 1701;1702;SKEXP0003;SKEXP0011;SKEXP0052; + + + + 1701;1702;SKEXP0003;SKEXP0011;SKEXP0052; + + + + + + + diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromContext.cs b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromContext.cs index 33e1023..2688828 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromContext.cs +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromContext.cs @@ -1,4 +1,4 @@ -using Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering; +using Encamina.Enmarcha.AI.OpenAI.Azure; using Encamina.Enmarcha.SemanticKernel.Abstractions; using Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering; @@ -14,7 +14,7 @@ internal static class ExampleQuestionAnsweringFromContext { public static async Task RunAsync() { - Console.WriteLine("# Executing Example_QuestionAnsweringFromContext"); + Console.WriteLine(@"# Executing Example_QuestionAnsweringFromContext"); // Create and configure builder var hostBuilder = new HostBuilder() @@ -27,21 +27,21 @@ public static async Task RunAsync() hostBuilder.ConfigureServices((hostContext, services) => { // Add Semantic Kernel options - services.AddOptions().Bind(hostContext.Configuration.GetSection(nameof(SemanticKernelOptions))).ValidateDataAnnotations().ValidateOnStart(); + services.AddOptions().Bind(hostContext.Configuration.GetSection(nameof(AzureOpenAIOptions))).ValidateDataAnnotations().ValidateOnStart(); services.AddScoped(sp => { // Get semantic kernel options - var options = hostContext.Configuration.GetRequiredSection(nameof(SemanticKernelOptions)).Get() - ?? throw new InvalidOperationException(@$"Missing configuration for {nameof(SemanticKernelOptions)}"); + var options = hostContext.Configuration.GetRequiredSection(nameof(AzureOpenAIOptions)).Get() + ?? throw new InvalidOperationException(@$"Missing configuration for {nameof(AzureOpenAIOptions)}"); // Initialize semantic kernel - var kernel = new KernelBuilder() - .WithAzureOpenAIChatCompletionService(options.ChatModelDeploymentName, options.Endpoint.ToString(), options.Key) + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(options.ChatModelDeploymentName, options.Endpoint.ToString(), options.Key) .Build(); // Import Question Answering plugin - kernel.ImportQuestionAnsweringPlugin(sp, ILengthFunctions.LengthByTokenCount); + kernel.ImportQuestionAnsweringPlugin(options, ILengthFunctions.LengthByTokenCount); return kernel; }); @@ -50,7 +50,7 @@ public static async Task RunAsync() var host = hostBuilder.Build(); // Initialize Q&A - var testQuestionAnswering = new TestQuestionAnswering(host.Services.GetRequiredService()); + var testQuestionAnswering = new TestQuestionAnswering(host.Services.GetRequiredService()); var result = await testQuestionAnswering.TestQuestionAnsweringFromContextAsync(); diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromMemory..cs b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromMemory..cs index adfb8fb..4a4ad91 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromMemory..cs +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/ExampleQuestionAnsweringFromMemory..cs @@ -1,4 +1,5 @@ -using Encamina.Enmarcha.SemanticKernel.Abstractions; +using Encamina.Enmarcha.AI.OpenAI.Azure; +using Encamina.Enmarcha.SemanticKernel.Abstractions; using Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering; using Microsoft.Extensions.Configuration; @@ -6,9 +7,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AI.OpenAI; using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Plugins.Memory; namespace Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering; @@ -29,26 +28,26 @@ public static async Task RunAsync() hostBuilder.ConfigureServices((hostContext, services) => { // Get semantic kernel options - var options = hostContext.Configuration.GetRequiredSection(nameof(SemanticKernelOptions)).Get() - ?? throw new InvalidOperationException(@$"Missing configuration for {nameof(SemanticKernelOptions)}"); + var options = hostContext.Configuration.GetRequiredSection(nameof(AzureOpenAIOptions)).Get() + ?? throw new InvalidOperationException(@$"Missing configuration for {nameof(AzureOpenAIOptions)}"); // Add Semantic Kernel options - services.AddOptions().Bind(hostContext.Configuration.GetSection(nameof(SemanticKernelOptions))).ValidateDataAnnotations().ValidateOnStart(); + services.AddOptions().Bind(hostContext.Configuration.GetSection(nameof(AzureOpenAIOptions))).ValidateDataAnnotations().ValidateOnStart(); // Here use any desired implementation (Qdrant, Volatile...) services.AddSingleton() - .AddSemanticTextMemory(); + .AddSemanticTextMemory(); services.AddScoped(sp => { // Initialize semantic kernel - var kernel = new KernelBuilder() - .WithAzureOpenAIChatCompletionService(options.ChatModelDeploymentName, options.Endpoint.ToString(), options.Key) - .WithAzureOpenAITextEmbeddingGenerationService(options.EmbeddingsModelDeploymentName, options.Endpoint.ToString(), options.Key) + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(options.ChatModelDeploymentName, options.Endpoint.ToString(), options.Key) + .AddAzureOpenAITextEmbeddingGeneration(options.EmbeddingsModelDeploymentName, options.Endpoint.ToString(), options.Key) .Build(); // Import Question Answering plugin - kernel.ImportQuestionAnsweringPluginWithMemory(sp, ILengthFunctions.LengthByTokenCount); + kernel.ImportQuestionAnsweringPluginWithMemory(options, sp.GetRequiredService(), ILengthFunctions.LengthByTokenCount); return kernel; }); @@ -64,7 +63,7 @@ public static async Task RunAsync() await mockMemoryInformation.SaveDataMockAsync(); // Initialize Q&A from Memory - var testQuestionAnswering = new TestQuestionAnswering(host.Services.GetService()); + var testQuestionAnswering = new TestQuestionAnswering(host.Services.GetService()); var result = await testQuestionAnswering.TestQuestionAnsweringFromMemoryAsync(); Console.WriteLine($@"RESULT: {result}"); diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Program.cs b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Program.cs index 7f67a94..6d426a1 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Program.cs +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/Program.cs @@ -2,7 +2,7 @@ internal static class Program { - private static async Task Main(string[] args) + private static async Task Main(string[] _) { await ExampleQuestionAnsweringFromContext.RunAsync(); await ExampleQuestionAnsweringFromMemory.RunAsync(); diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/TestQuestionAnswering.cs b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/TestQuestionAnswering.cs index 32dbeef..3473a2f 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/TestQuestionAnswering.cs +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/TestQuestionAnswering.cs @@ -1,7 +1,6 @@ using Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Orchestration; namespace Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering; @@ -10,9 +9,9 @@ namespace Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering; /// internal class TestQuestionAnswering { - private readonly IKernel kernel; + private readonly Kernel kernel; - public TestQuestionAnswering(IKernel kernel) + public TestQuestionAnswering(Kernel kernel) { this.kernel = kernel; } @@ -31,13 +30,14 @@ public async Task TestQuestionAnsweringFromContextAsync() Console.WriteLine($"# Context: {context} \n"); Console.WriteLine($"# Question: {input} \n"); - var contextVariables = new ContextVariables(); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Parameters.Input, input); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Parameters.Context, context); + var arguments = new KernelArguments() + { + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Parameters.Input] = input, + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Parameters.Context] = context, + }; - var functionQuestionAnswering = kernel.Functions.GetFunction(PluginsInfo.QuestionAnsweringPlugin.Name, PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Name); - var resultContext = await kernel.RunAsync(contextVariables, functionQuestionAnswering); - var result = resultContext.GetValue(); + var functionQuestionAnswering = kernel.Plugins.GetFunction(PluginsInfo.QuestionAnsweringPlugin.Name, PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Name); + var result = await kernel.InvokeAsync(functionQuestionAnswering, arguments); Console.WriteLine($"# Result: {result} \n"); @@ -47,19 +47,20 @@ public async Task TestQuestionAnsweringFromContextAsync() public async Task TestQuestionAnsweringFromMemoryAsync() { var input = "What period occurred the Industrial Revolution?"; - var contextVariables = new ContextVariables(); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.Question, input); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.CollectionSeparator, ","); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.CollectionsStr, "my-collection"); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.MinRelevance, "0.8"); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.ResultsLimit, "1"); - contextVariables.Set(PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.ResponseTokenLimit, "300"); + var arguments = new KernelArguments() + { + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.Question] = input, + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.CollectionSeparator] = ",", + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.CollectionsStr] = "my-collection", + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.MinRelevance] = 0.8, + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.ResultsLimit] = 1, + [PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Parameters.ResponseTokenLimit] = 300 + }; Console.WriteLine($"# Question: {input} \n"); - var functionQuestionAnswering = kernel.Functions.GetFunction(PluginsInfo.QuestionAnsweringPlugin.Name, PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Name); - var resultContext = await kernel.RunAsync(contextVariables, functionQuestionAnswering); - var result = resultContext.GetValue(); + var functionQuestionAnswering = kernel.Plugins.GetFunction(PluginsInfo.QuestionAnsweringPlugin.Name, PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromMemoryQuery.Name); + var result = await kernel.InvokeAsync(functionQuestionAnswering, arguments); Console.WriteLine($"# Result: {result} \n"); diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/appsettings.json b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/appsettings.json index 12ced34..938927e 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/appsettings.json +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.QuestionAnswering/appsettings.json @@ -1,5 +1,5 @@ { - "SemanticKernelOptions": { + "AzureOpenAIOptions": { "ChatModelName": "", // Name (sort of a unique identifier) of the model to use for chat. "ChatModelDeploymentName": "", // Model deployment name on the LLM (for example OpenAI) to use for chat. "EmbeddingsModelName": "", // Name (sort of a unique identifier) of the model to use for embeddings. diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Encamina.Enmarcha.Samples.SemanticKernel.Text.csproj b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Encamina.Enmarcha.Samples.SemanticKernel.Text.csproj index 3e79989..8c27e7e 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Encamina.Enmarcha.Samples.SemanticKernel.Text.csproj +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Encamina.Enmarcha.Samples.SemanticKernel.Text.csproj @@ -14,6 +14,7 @@ + diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Example.cs b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Example.cs index 7d04bd6..1dd1025 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Example.cs +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Example.cs @@ -1,13 +1,12 @@ using Encamina.Enmarcha.SemanticKernel.Plugins.Text; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Orchestration; namespace Encamina.Enmarcha.Samples.SemanticKernel.Text; internal class Example { - private readonly IKernel kernel; + private readonly Kernel kernel; private readonly string input = @"Alexandre Dumas born Dumas Davy de la Pailleterie, 24 July 1802 – 5 December 1870), also known as Alexandre Dumas père, was a French novelist and playwright. His works have been translated into many languages and he is one of the most widely read French authors. Many of his historical novels of adventure were originally published as serials, including The Count of Monte Cristo, The Three Musketeers, Twenty Years After and The Vicomte of Bragelonne: Ten Years Later.Since the early 20th century, his novels have been adapted into nearly 200 films.Prolific in several genres, Dumas began his career by writing plays, which were successfully produced from the first. He wrote numerous magazine articles and travel books; his published works totalled 100,000 pages.In the 1840s, Dumas founded the Théâtre Historique in Paris. @@ -16,7 +15,7 @@ internal class Example English playwright Watts Phillips, who knew Dumas in his later life, described him as ""the most generous, large - hearted being in the world.He also was the most delightfully amusing and egotistical creature on the face of the earth.His tongue was like a windmill – once set in motion, you never knew when he would stop, especially if the theme was himself."""; /// - public Example(IKernel kernel) + public Example(Kernel kernel) { this.kernel = kernel; Console.WriteLine($"# Context: {input} \n"); @@ -25,14 +24,15 @@ public Example(IKernel kernel) /// public async Task TestSummaryAsync() { - var contextVariables = new ContextVariables(); - contextVariables.Set(PluginsInfo.TextPlugin.Functions.Summarize.Parameters.Input, input); - contextVariables.Set(PluginsInfo.TextPlugin.Functions.Summarize.Parameters.MaxWordsCount, "15"); + var summaryArguments = new KernelArguments() + { + [PluginsInfo.TextPlugin.Functions.Summarize.Parameters.Input] = input, + [PluginsInfo.TextPlugin.Functions.Summarize.Parameters.MaxWordsCount] = 15, + }; - var functionSummarize = kernel.Functions.GetFunction(PluginsInfo.TextPlugin.Name, PluginsInfo.TextPlugin.Functions.Summarize.Name); + var functionSummarize = kernel.Plugins.GetFunction(PluginsInfo.TextPlugin.Name, PluginsInfo.TextPlugin.Functions.Summarize.Name); - var resultContext = await kernel.RunAsync(contextVariables, functionSummarize); - var result = resultContext.GetValue(); + var result = await kernel.InvokeAsync(functionSummarize, summaryArguments); Console.WriteLine($"# Summary: {result} \n"); } @@ -40,14 +40,15 @@ public async Task TestSummaryAsync() /// public async Task TextKeyPhrasesAsync() { - var contextVariables = new ContextVariables(); - contextVariables.Set(PluginsInfo.TextPlugin.Functions.KeyPhrases.Parameters.Input, input); - contextVariables.Set(PluginsInfo.TextPlugin.Functions.KeyPhrases.Parameters.TopKeyphrases, "2"); + var keyPhrasesArguments = new KernelArguments() + { + [PluginsInfo.TextPlugin.Functions.KeyPhrases.Parameters.Input] = input, + [PluginsInfo.TextPlugin.Functions.KeyPhrases.Parameters.TopKeyphrases] = 2, + }; - var functionSummarize = kernel.Functions.GetFunction(PluginsInfo.TextPlugin.Name, PluginsInfo.TextPlugin.Functions.KeyPhrases.Name); + var functionSummarize = kernel.Plugins.GetFunction(PluginsInfo.TextPlugin.Name, PluginsInfo.TextPlugin.Functions.KeyPhrases.Name); - var resultContext = await kernel.RunAsync(contextVariables, functionSummarize); - var result = resultContext.GetValue(); + var result = await kernel.InvokeAsync(functionSummarize, keyPhrasesArguments); Console.WriteLine($"# Key Phrases: {result} \n"); } diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Program.cs b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Program.cs index a23c631..db4710f 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Program.cs +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/Program.cs @@ -1,4 +1,4 @@ -using Encamina.Enmarcha.SemanticKernel.Abstractions; +using Encamina.Enmarcha.AI.OpenAI.Azure; using Encamina.Enmarcha.SemanticKernel.Plugins.Text; using Microsoft.Extensions.Configuration; @@ -11,7 +11,7 @@ namespace Encamina.Enmarcha.Samples.SemanticKernel.Text; internal static class Program { - private static async Task Main(string[] args) + private static async Task Main(string[] _) { // Create and configure builder var hostBuilder = new HostBuilder() @@ -23,15 +23,15 @@ private static async Task Main(string[] args) // Configure service hostBuilder.ConfigureServices((hostContext, services) => { - services.AddScoped(sp => + services.AddScoped(_ => { // Get semantic kernel options - var options = hostContext.Configuration.GetRequiredSection(nameof(SemanticKernelOptions)).Get() - ?? throw new InvalidOperationException(@$"Missing configuration for {nameof(SemanticKernelOptions)}"); + var options = hostContext.Configuration.GetRequiredSection(nameof(AzureOpenAIOptions)).Get() + ?? throw new InvalidOperationException(@$"Missing configuration for {nameof(AzureOpenAIOptions)}"); // Initialize semantic kernel - var kernel = new KernelBuilder() - .WithAzureOpenAIChatCompletionService(options.ChatModelDeploymentName, options.Endpoint.ToString(), options.Key) + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(options.ChatModelDeploymentName, options.Endpoint.ToString(), options.Key) .Build(); kernel.ImportTextPlugin(); @@ -43,7 +43,7 @@ private static async Task Main(string[] args) var host = hostBuilder.Build(); // Initialize Examples - var example = new Example(host.Services.GetRequiredService()); + var example = new Example(host.Services.GetRequiredService()); await example.TestSummaryAsync(); diff --git a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/appsettings.json b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/appsettings.json index 6ec609b..db1cbc9 100644 --- a/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/appsettings.json +++ b/samples/SemanticKernel/Encamina.Enmarcha.Samples.SemanticKernel.Text/appsettings.json @@ -1,5 +1,5 @@ { - "SemanticKernelOptions": { + "AzureOpenAIOptions": { "ChatModelName": "", // Name (sort of a unique identifier) of the model to use for chat. "ChatModelDeploymentName": "", // Model deployment name on the LLM (for example OpenAI) to use for chat. "Endpoint": "", // URL for an LLM resource (like OpenAI). This should include protocol and host name. diff --git a/src/Encamina.Enmarcha.AI.Abstractions/Encamina.Enmarcha.AI.Abstractions.csproj b/src/Encamina.Enmarcha.AI.Abstractions/Encamina.Enmarcha.AI.Abstractions.csproj index 86f5983..d43b3a0 100644 --- a/src/Encamina.Enmarcha.AI.Abstractions/Encamina.Enmarcha.AI.Abstractions.csproj +++ b/src/Encamina.Enmarcha.AI.Abstractions/Encamina.Enmarcha.AI.Abstractions.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Encamina.Enmarcha.AI.IntentsPrediction.Abstractions/IntentKindBase.cs b/src/Encamina.Enmarcha.AI.IntentsPrediction.Abstractions/IntentKindBase.cs index f13c0af..de3d2ca 100644 --- a/src/Encamina.Enmarcha.AI.IntentsPrediction.Abstractions/IntentKindBase.cs +++ b/src/Encamina.Enmarcha.AI.IntentsPrediction.Abstractions/IntentKindBase.cs @@ -8,7 +8,7 @@ namespace Encamina.Enmarcha.AI.IntentsPrediction.Abstractions; /// [SuppressMessage(@"Major Code Smell", "S4035:Classes implementing \"IEquatable\" should be sealed", - Justification = "Intended useage of this class requires it to be inheritable and implement the \"IEquatable\" interface!")] + Justification = "Intended usage of this class requires it to be inheritable and implement the \"IEquatable\" interface!")] public class IntentKindBase : IEqualityComparer, IEquatable { private readonly string value; @@ -29,8 +29,8 @@ protected IntentKindBase(string value) /// /// Determines if two values are the same. /// - /// The left part from the equality comparisson. - /// The right part from the equality comparisson. + /// The left part from the equality comparison. + /// The right part from the equality comparison. /// /// Returns if is the same as , otherwise returns . /// diff --git a/src/Encamina.Enmarcha.AI.IntentsPrediction.Azure/Encamina.Enmarcha.AI.IntentsPrediction.Azure.csproj b/src/Encamina.Enmarcha.AI.IntentsPrediction.Azure/Encamina.Enmarcha.AI.IntentsPrediction.Azure.csproj index aabd661..9f71e67 100644 --- a/src/Encamina.Enmarcha.AI.IntentsPrediction.Azure/Encamina.Enmarcha.AI.IntentsPrediction.Azure.csproj +++ b/src/Encamina.Enmarcha.AI.IntentsPrediction.Azure/Encamina.Enmarcha.AI.IntentsPrediction.Azure.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Completition.cs b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Completion.cs similarity index 79% rename from src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Completition.cs rename to src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Completion.cs index 513d8a2..25bec67 100644 --- a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Completition.cs +++ b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Completion.cs @@ -1,12 +1,12 @@ namespace Encamina.Enmarcha.AI.OpenAI.Abstractions; /// -/// Represents a completition generated by OpenAI. +/// Represents a completion generated by OpenAI. /// -public class Completition +public class Completion { /// - /// Gets the text of the generatad completion . + /// Gets the text of the generated completion . /// public string Text { get; init; } diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/CompletionResult.cs b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/CompletionResult.cs index a483adb..9ef54a6 100644 --- a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/CompletionResult.cs +++ b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/CompletionResult.cs @@ -15,5 +15,5 @@ public class CompletionResult : IdentifiableBase /// /// Gets the completions generated. /// - public IEnumerable Completitions { get; init; } + public IEnumerable Completitions { get; init; } } diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Encamina.Enmarcha.AI.OpenAI.Abstractions.csproj b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Encamina.Enmarcha.AI.OpenAI.Abstractions.csproj index 3ea88f6..cb3b465 100644 --- a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Encamina.Enmarcha.AI.OpenAI.Abstractions.csproj +++ b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/Encamina.Enmarcha.AI.OpenAI.Abstractions.csproj @@ -1,10 +1,11 @@ - + netstandard2.1 + diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/OpenAIOptions.cs b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/OpenAIOptions.cs new file mode 100644 index 0000000..a17d0cc --- /dev/null +++ b/src/Encamina.Enmarcha.AI.OpenAI.Abstractions/OpenAIOptions.cs @@ -0,0 +1,81 @@ +using System.ComponentModel.DataAnnotations; + +using Encamina.Enmarcha.Core.DataAnnotations; + +namespace Encamina.Enmarcha.AI.OpenAI.Abstractions; + +/// +/// Base class with options to connect and use an OpenAI service. +/// +public class OpenAIOptions +{ + /// + /// Gets the model deployment name on the LLM (for example OpenAI) to use for chat. + /// + /// + /// WARNING: The model deployment name does not necessarily have to be the same as the model name. For example, a model of type `gpt-4` might be called «MyGPT»; + /// this means that the value of this property does not necessarily indicate the model implemented behind it. Use property to set the model name. + /// + [NotEmptyOrWhitespace] + public string ChatModelDeploymentName { get; init; } + + /// + /// Gets the name (sort of a unique identifier) of the model to use for chat. + /// + /// + /// This property is required if property is not . It is usually used with the Encamina.Enmarcha.AI.OpenAI.Abstractions.ModelInfo" class + /// to get metadata and information about the model. This model name must match the model names from the LLM (like OpenAI), like for example `gpt-4` or `gpt-35-turbo`. + /// + [RequireWhenOtherPropertyNotNull(nameof(ChatModelDeploymentName))] + [NotEmptyOrWhitespace] + public string ChatModelName { get; init; } + + /// + /// Gets the model deployment name on the LLM (for example OpenAI) to use for completions. + /// + /// + /// WARNING: The model name does not necessarily have to be the same as the model ID. For example, a model of type `text-davinci-003` might be called `MyCompletions`; + /// this means that the value of this property does not necessarily indicate the model implemented behind it. Use property to set the model name. + /// + [NotEmptyOrWhitespace] + public string CompletionsModelDeploymentName { get; init; } + + /// + /// Gets the name (sort of a unique identifier) of the model to use for completions. + /// + /// + /// This property is required if property is not . It is usually used with the Encamina.Enmarcha.AI.OpenAI.Abstractions.ModelInfo" class + /// to get metadata and information about the model. This model name must match the model names from the LLM (like OpenAI), like for example `gpt-4` or `gpt-35-turbo`. + /// + [RequireWhenOtherPropertyNotNull(nameof(CompletionsModelDeploymentName))] + [NotEmptyOrWhitespace] + public string CompletionsModelName { get; init; } + + /// + /// Gets the model deployment name on the LLM (for example OpenAI) to use for embeddings. + /// + /// + /// WARNING: The model name does not necessarily have to be the same as the model ID. For example, a model of type `text-embedding-ada-002` might be called `MyEmbeddings`; + /// this means that the value of this property does not necessarily indicate the model implemented behind it. Use property to set the model name. + /// + [NotEmptyOrWhitespace] + public string EmbeddingsModelDeploymentName { get; init; } + + /// + /// Gets the name (sort of a unique identifier) of the model to use for embeddings. + /// + /// + /// This property is required if property is not . It is usually used with the Encamina.Enmarcha.AI.OpenAI.Abstractions.ModelInfo" class + /// to get metadata and information about the model. This model name must match the model names from the LLM (like OpenAI), like for example `gpt-4` or `gpt-35-turbo`. + /// + [RequireWhenOtherPropertyNotNull(nameof(EmbeddingsModelDeploymentName))] + [NotEmptyOrWhitespace] + public string EmbeddingsModelName { get; init; } + + /// + /// Gets the key credential used to authenticate to an LLM resource. + /// + [Required] + [NotEmptyOrWhitespace] + public string Key { get; init; } +} diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Azure/AzureOpenAIOptions.cs b/src/Encamina.Enmarcha.AI.OpenAI.Azure/AzureOpenAIOptions.cs new file mode 100644 index 0000000..c7064d5 --- /dev/null +++ b/src/Encamina.Enmarcha.AI.OpenAI.Azure/AzureOpenAIOptions.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +using Azure.AI.OpenAI; + +using Encamina.Enmarcha.AI.OpenAI.Abstractions; + +using Encamina.Enmarcha.Core.DataAnnotations; + +namespace Encamina.Enmarcha.AI.OpenAI.Azure; + +/// +/// Configuration options for Azure OpenAI service connection. +/// +public sealed class AzureOpenAIOptions : OpenAIOptions +{ + /// + /// Gets the Azure OpenAI API service version. + /// + public OpenAIClientOptions.ServiceVersion ServiceVersion { get; init; } = OpenAIClientOptions.ServiceVersion.V2023_12_01_Preview; + + /// + /// Gets the for an LLM resource (like OpenAI). This should include protocol and host name. + /// + [Required] + [Uri] + public Uri Endpoint { get; init; } +} + diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Azure/CompletionService.cs b/src/Encamina.Enmarcha.AI.OpenAI.Azure/CompletionService.cs index 32f47fb..b8e8a51 100644 --- a/src/Encamina.Enmarcha.AI.OpenAI.Azure/CompletionService.cs +++ b/src/Encamina.Enmarcha.AI.OpenAI.Azure/CompletionService.cs @@ -41,6 +41,7 @@ public async Task CompleteAsync(CompletionRequest request, Can var completionsOptions = new CompletionsOptions() { ChoicesPerPrompt = request.NumberOfCompletionsPerPrompt, + DeploymentName = options.DeploymentName, Echo = request.DoEcho, FrequencyPenalty = request.FrequencyPenalty, GenerationSampleCount = request.BestOf, @@ -54,13 +55,13 @@ public async Task CompleteAsync(CompletionRequest request, Can completionsOptions.Prompts.AddRange(request.Prompts); completionsOptions.StopSequences.AddRange(request.StopSequences); - var response = (await client.GetCompletionsAsync(options.DeploymentName, completionsOptions, cancellationToken)).Value; // Any error while calling Azure OpenAI is handled and thrown by the `GetCompletionsAsync` method itself... + var response = (await client.GetCompletionsAsync(completionsOptions, cancellationToken)).Value; // Any error while calling Azure OpenAI is handled and thrown by the `GetCompletionsAsync` method itself... return new CompletionResult() { Id = response.Id, CreatedUtc = response.Created.UtcDateTime, - Completitions = response.Choices.Select(choice => new Completition() + Completitions = response.Choices.Select(choice => new Completion() { Text = choice.Text, Index = choice.Index, diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Azure/Encamina.Enmarcha.AI.OpenAI.Azure.csproj b/src/Encamina.Enmarcha.AI.OpenAI.Azure/Encamina.Enmarcha.AI.OpenAI.Azure.csproj index 2576685..c204292 100644 --- a/src/Encamina.Enmarcha.AI.OpenAI.Azure/Encamina.Enmarcha.AI.OpenAI.Azure.csproj +++ b/src/Encamina.Enmarcha.AI.OpenAI.Azure/Encamina.Enmarcha.AI.OpenAI.Azure.csproj @@ -5,10 +5,10 @@ - + - + diff --git a/src/Encamina.Enmarcha.AI.OpenAI.Azure/Properties/IsExternalInit.cs b/src/Encamina.Enmarcha.AI.OpenAI.Azure/Properties/IsExternalInit.cs new file mode 100644 index 0000000..6649051 --- /dev/null +++ b/src/Encamina.Enmarcha.AI.OpenAI.Azure/Properties/IsExternalInit.cs @@ -0,0 +1,16 @@ +#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ +} + +#endif \ No newline at end of file diff --git a/src/Encamina.Enmarcha.AI.TextsTranslation.Azure/Encamina.Enmarcha.AI.TextsTranslation.Azure.csproj b/src/Encamina.Enmarcha.AI.TextsTranslation.Azure/Encamina.Enmarcha.AI.TextsTranslation.Azure.csproj index 0f926c8..c145f79 100644 --- a/src/Encamina.Enmarcha.AI.TextsTranslation.Azure/Encamina.Enmarcha.AI.TextsTranslation.Azure.csproj +++ b/src/Encamina.Enmarcha.AI.TextsTranslation.Azure/Encamina.Enmarcha.AI.TextsTranslation.Azure.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Encamina.Enmarcha.AI/Encamina.Enmarcha.AI.csproj b/src/Encamina.Enmarcha.AI/Encamina.Enmarcha.AI.csproj index 17a75ba..f0e501c 100644 --- a/src/Encamina.Enmarcha.AI/Encamina.Enmarcha.AI.csproj +++ b/src/Encamina.Enmarcha.AI/Encamina.Enmarcha.AI.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Encamina.Enmarcha.AspNet.Mvc/Encamina.Enmarcha.AspNet.Mvc.csproj b/src/Encamina.Enmarcha.AspNet.Mvc/Encamina.Enmarcha.AspNet.Mvc.csproj index 774e547..af8842b 100644 --- a/src/Encamina.Enmarcha.AspNet.Mvc/Encamina.Enmarcha.AspNet.Mvc.csproj +++ b/src/Encamina.Enmarcha.AspNet.Mvc/Encamina.Enmarcha.AspNet.Mvc.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle.csproj b/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle.csproj index 06eda62..651b002 100644 --- a/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle.csproj +++ b/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,8 @@ true - - + + diff --git a/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/VersionSwaggerGenConfigureOptions.cs b/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/VersionSwaggerGenConfigureOptions.cs index acbdc95..c0a39fb 100644 --- a/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/VersionSwaggerGenConfigureOptions.cs +++ b/src/Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle/VersionSwaggerGenConfigureOptions.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; using Microsoft.Extensions.Options; diff --git a/src/Encamina.Enmarcha.Core/Encamina.Enmarcha.Core.csproj b/src/Encamina.Enmarcha.Core/Encamina.Enmarcha.Core.csproj index 0092213..7f4b664 100644 --- a/src/Encamina.Enmarcha.Core/Encamina.Enmarcha.Core.csproj +++ b/src/Encamina.Enmarcha.Core/Encamina.Enmarcha.Core.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Encamina.Enmarcha.Data.Cosmos/Encamina.Enmarcha.Data.Cosmos.csproj b/src/Encamina.Enmarcha.Data.Cosmos/Encamina.Enmarcha.Data.Cosmos.csproj index bb5ddcc..cd40227 100644 --- a/src/Encamina.Enmarcha.Data.Cosmos/Encamina.Enmarcha.Data.Cosmos.csproj +++ b/src/Encamina.Enmarcha.Data.Cosmos/Encamina.Enmarcha.Data.Cosmos.csproj @@ -9,10 +9,10 @@ - + - + diff --git a/src/Encamina.Enmarcha.Data.EntityFramework/Encamina.Enmarcha.Data.EntityFramework.csproj b/src/Encamina.Enmarcha.Data.EntityFramework/Encamina.Enmarcha.Data.EntityFramework.csproj index 46705b0..9536a5f 100644 --- a/src/Encamina.Enmarcha.Data.EntityFramework/Encamina.Enmarcha.Data.EntityFramework.csproj +++ b/src/Encamina.Enmarcha.Data.EntityFramework/Encamina.Enmarcha.Data.EntityFramework.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/Encamina.Enmarcha.SemanticKernel.Abstractions.csproj b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/Encamina.Enmarcha.SemanticKernel.Abstractions.csproj index 6909143..c8c631d 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/Encamina.Enmarcha.SemanticKernel.Abstractions.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/Encamina.Enmarcha.SemanticKernel.Abstractions.csproj @@ -4,10 +4,18 @@ netstandard2.1 + + 1701;1702;SKEXP0003 + + + + 1701;1702;SKEXP0003 + + - - - + + + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/IMemoryStoreHandler.cs b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/IMemoryStoreHandler.cs index d634524..5cf6f1f 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/IMemoryStoreHandler.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/IMemoryStoreHandler.cs @@ -21,7 +21,7 @@ public interface IMemoryStoreHandler /// /// Gets the name of a collection from its unique identifier. /// - /// The unique identifier of the collaction. + /// The unique identifier of the collection. /// A cancellation token that can be used to receive notice of cancellation. /// The name of the collection. Task GetCollectionNameAsync(string collectionId, CancellationToken cancellationToken); diff --git a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/MemoryStoreHandlerBase.cs b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/MemoryStoreHandlerBase.cs index bbfd216..70627d5 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/MemoryStoreHandlerBase.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/MemoryStoreHandlerBase.cs @@ -1,4 +1,6 @@ -using System.Collections.Concurrent; +// Ignore Spelling: Utc + +using System.Collections.Concurrent; using Microsoft.SemanticKernel.Memory; @@ -13,7 +15,9 @@ public abstract class MemoryStoreHandlerBase : IMemoryStoreHandler /// Initializes a new instance of the class. /// /// The to handle. +#pragma warning disable SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. protected MemoryStoreHandlerBase(IMemoryStore memoryStore) +#pragma warning restore SKEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. { MemoryStore = memoryStore; } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/SKContextExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Abstractions/SKContextExtensions.cs deleted file mode 100644 index 82ca7c4..0000000 --- a/src/Encamina.Enmarcha.SemanticKernel.Abstractions/SKContextExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.SemanticKernel.Orchestration; - -namespace Encamina.Enmarcha.SemanticKernel.Abstractions; - -/// -/// Utility methods for Semantic Kernel context. -/// -public static class SKContextExtensions -{ - /// - /// Search the value of the given variable in the context. - /// - /// The SKContext object. - /// The name of the variable to retrieve. - /// Whether or not to throw an exception if the variable is not found. - /// The value of the variable if found, null or exception otherwise. - public static string GetContextVariable(this SKContext context, string variableName, bool throwExceptionIfNotFound = false) - { - if (context.Variables.ContainsKey(variableName)) - { - return context.Variables[variableName]; - } - - return throwExceptionIfNotFound - ? throw new ArgumentException($"Variable {variableName} not found in SK Context.") - : null; - } -} \ No newline at end of file diff --git a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Connectors/StrictFormatCleanPdfDocumentConnector.cs b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Connectors/StrictFormatCleanPdfDocumentConnector.cs index d388114..6b1b10e 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Connectors/StrictFormatCleanPdfDocumentConnector.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Connectors/StrictFormatCleanPdfDocumentConnector.cs @@ -64,7 +64,7 @@ private static IEnumerable GetTextBlocks(Page page) var pageSegmenter = new DocstrumBoundingBoxes(pageSegmenterOptions); var textBlocks = pageSegmenter.GetBlocks(words); - // 3. Postprocessing + // 3. Post-processing var orderedTextBlocks = RenderingReadingOrderDetector.Instance.Get(textBlocks); return orderedTextBlocks; diff --git a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Encamina.Enmarcha.SemanticKernel.Connectors.Document.csproj b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Encamina.Enmarcha.SemanticKernel.Connectors.Document.csproj index 1043ab5..041aa13 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Encamina.Enmarcha.SemanticKernel.Connectors.Document.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Document/Encamina.Enmarcha.SemanticKernel.Connectors.Document.csproj @@ -6,8 +6,16 @@ enable + + 1701;1702;SKEXP0051 + + + + 1701;1702;SKEXP0051 + + - + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Encamina.Enmarcha.SemanticKernel.Connectors.Memory.csproj b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Encamina.Enmarcha.SemanticKernel.Connectors.Memory.csproj index e0b8aff..298f614 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Encamina.Enmarcha.SemanticKernel.Connectors.Memory.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Encamina.Enmarcha.SemanticKernel.Connectors.Memory.csproj @@ -6,13 +6,23 @@ enable + + 1701;1702;SKEXP0003;SKEXP0011;SKEXP0026 + + + + 1701;1702;SKEXP0003;SKEXP0011;SKEXP0026 + + - + + + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Extensions/IServiceCollectionExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Extensions/IServiceCollectionExtensions.cs index 3a9fbf1..6959bfb 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Extensions/IServiceCollectionExtensions.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/Extensions/IServiceCollectionExtensions.cs @@ -1,15 +1,13 @@ -using Encamina.Enmarcha.Data.Qdrant.Abstractions; +using Encamina.Enmarcha.AI.OpenAI.Azure; +using Encamina.Enmarcha.Data.Qdrant.Abstractions; using Encamina.Enmarcha.Data.Qdrant.Abstractions.Extensions; -using Encamina.Enmarcha.SemanticKernel.Abstractions; - using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.SemanticKernel.Connectors.AI.OpenAI; -using Microsoft.SemanticKernel.Connectors.Memory.Qdrant; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Plugins.Memory; namespace Microsoft.Extensions.DependencyInjection; @@ -63,10 +61,10 @@ public static IServiceCollection AddSemanticTextMemory(this IServiceCollection s { return services.TryAddType(serviceLifetime, sp => { - var options = sp.GetRequiredService>().Value; + var options = sp.GetRequiredService>().Value; return new MemoryBuilder() - .WithAzureOpenAITextEmbeddingGenerationService(options.EmbeddingsModelDeploymentName, options.Endpoint.ToString(), options.Key) + .WithAzureOpenAITextEmbeddingGeneration(options.EmbeddingsModelDeploymentName, options.Endpoint.ToString(), options.Key) .WithMemoryStore(sp.GetRequiredService()) .Build(); }); diff --git a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/README.md b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/README.md index 485329d..2de74c7 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/README.md +++ b/src/Encamina.Enmarcha.SemanticKernel.Connectors.Memory/README.md @@ -79,9 +79,9 @@ Once configured, you can now use Semantic Kernel, and it will utilize the Qdrant ```csharp public class MyClass { - private readonly IKernel kernel; + private readonly Kernel kernel; - public MyClass(IKernel kernel) + public MyClass(Kernel kernel) { this.kernel = kernel; } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Encamina.Enmarcha.SemanticKernel.Plugins.Chat.csproj b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Encamina.Enmarcha.SemanticKernel.Plugins.Chat.csproj index a2603a8..78072ca 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Encamina.Enmarcha.SemanticKernel.Plugins.Chat.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Encamina.Enmarcha.SemanticKernel.Plugins.Chat.csproj @@ -7,13 +7,14 @@ - - + + + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/IKernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/KernelExtensions.cs similarity index 67% rename from src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/IKernelExtensions.cs rename to src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/KernelExtensions.cs index e2f51d2..f370783 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/IKernelExtensions.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/KernelExtensions.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Diagnostics; +using Encamina.Enmarcha.AI.OpenAI.Abstractions; using Encamina.Enmarcha.Data.Abstractions; using Encamina.Enmarcha.Data.Cosmos; @@ -14,24 +15,17 @@ namespace Encamina.Enmarcha.SemanticKernel.Plugins.Chat; /// -/// Extension methods on to import and configure plugins. +/// Extension methods on to import and configure plugins. /// -public static class IKernelExtensions +public static class KernelExtensions { /// - /// Imports the «Chat with History» plugin into the kernel. + /// Imports the «Chat with History» plugin into the . /// /// /// This extension method uses a «Service Location» pattern provided by the to resolve the following dependencies: /// /// - /// SemanticKernelOptions - /// - /// A required dependency of type used to retrieve the configurations for the . This dependency - /// should be added using any of the extension method. - /// - /// - /// /// ChatWithHistoryPluginOptions /// /// A required dependency of type used to retrieve the configuration options for this plugin. This dependency should @@ -47,25 +41,25 @@ public static class IKernelExtensions /// /// /// - /// The instance to add this plugin. + /// The instance to add this plugin. /// A to resolve the dependencies. + /// Configuration options for OpenAI services. /// The name of the Cosmos DB container to store the chat history messages. /// /// A function to calculate the length by tokens of the chat messages. These functions are usually available in the «mixin» interface . /// /// A list of all the functions found in this plugin, indexed by function name. - public static IDictionary ImportChatWithHistoryPluginUsingCosmosDb(this IKernel kernel, IServiceProvider serviceProvider, string cosmosContainer, Func tokensLengthFunction) + public static KernelPlugin ImportChatWithHistoryPluginUsingCosmosDb(this Kernel kernel, IServiceProvider serviceProvider, OpenAIOptions openAIOptions, string cosmosContainer, Func tokensLengthFunction) { Guard.IsNotNull(serviceProvider); Guard.IsNotNull(tokensLengthFunction); Guard.IsNotNullOrWhiteSpace(cosmosContainer); - var semanticKernelOptions = serviceProvider.GetRequiredService>().CurrentValue; var chatWithHistoryPluginOptions = serviceProvider.GetRequiredService>(); var chatMessagesHistoryRepository = serviceProvider.GetRequiredService().Create(cosmosContainer); - var chatWithHistoryPlugin = new ChatWithHistoryPlugin(kernel, semanticKernelOptions.ChatModelName, tokensLengthFunction, chatMessagesHistoryRepository, chatWithHistoryPluginOptions); + var chatWithHistoryPlugin = new ChatWithHistoryPlugin(kernel, openAIOptions.ChatModelName, tokensLengthFunction, chatMessagesHistoryRepository, chatWithHistoryPluginOptions); - return kernel.ImportFunctions(chatWithHistoryPlugin, PluginsInfo.ChatWithHistoryPlugin.Name); + return kernel.ImportPluginFromObject(chatWithHistoryPlugin, PluginsInfo.ChatWithHistoryPlugin.Name); } } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPlugin.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPlugin.cs index ff4747f..f4d73ef 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPlugin.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPlugin.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Options; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; namespace Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Plugins; @@ -16,9 +16,8 @@ namespace Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Plugins; public class ChatWithHistoryPlugin { private readonly string chatModelName; - private readonly IChatCompletion chat; private readonly IAsyncRepository chatMessagesHistoryRepository; - + private readonly Kernel kernel; private readonly Func tokensLengthFunction; private ChatWithHistoryPluginOptions options; @@ -31,9 +30,9 @@ public class ChatWithHistoryPlugin /// Function to calculate the length of a string (usually the chat messages) in tokens. /// A valid instance of an asynchronous repository pattern implementation. /// Configuration options for this plugin. - public ChatWithHistoryPlugin(IKernel kernel, string chatModelName, Func tokensLengthFunction, IAsyncRepository chatMessagesHistoryRepository, IOptionsMonitor options) + public ChatWithHistoryPlugin(Kernel kernel, string chatModelName, Func tokensLengthFunction, IAsyncRepository chatMessagesHistoryRepository, IOptionsMonitor options) { - this.chat = kernel.GetService(); + this.kernel = kernel; this.chatModelName = chatModelName; this.chatMessagesHistoryRepository = chatMessagesHistoryRepository; this.options = options.CurrentValue; @@ -58,7 +57,7 @@ public ChatWithHistoryPlugin(IKernel kernel, string chatModelName, FuncThe preferred language of the user while chatting. /// A cancellation token that can be used to receive notice of cancellation. /// A string representing the response from the Artificial Intelligence. - [SKFunction] + [KernelFunction] [Description(@"Allows users to chat and ask questions to an Artificial Intelligence.")] public virtual async Task ChatAsync( [Description(@"What the user says or asks when chatting")] string ask, @@ -70,12 +69,12 @@ public virtual async Task ChatAsync( var systemPrompt = $@"{SystemPrompt} The name of the user is {userName}. The user prefers responses using the language identified as {locale}. Always answer using {locale} as language."; var chatModelMaxTokens = ModelInfo.GetById(chatModelName).MaxTokens; - var askTokens = tokensLengthFunction(ask); - var systemPromptTokens = tokensLengthFunction(systemPrompt); + var askTokens = GetChatMessageTokenCount(AuthorRole.User, ask); + var systemPromptTokens = GetChatMessageTokenCount(AuthorRole.System, systemPrompt); var remainingTokens = chatModelMaxTokens - askTokens - systemPromptTokens - (options.ChatRequestSettings.MaxTokens ?? 0); - var chatHistory = chat.CreateNewChat(systemPrompt); + var chatHistory = new ChatHistory(systemPrompt); if (remainingTokens < 0) { @@ -86,7 +85,8 @@ public virtual async Task ChatAsync( chatHistory.AddUserMessage(ask); - var response = await chat.GenerateMessageAsync(chatHistory, options.ChatRequestSettings, cancellationToken); + var chatMessage = await kernel.GetRequiredService().GetChatMessageContentAsync(chatHistory, options.ChatRequestSettings, kernel, cancellationToken); + var response = chatMessage.Content; await SaveChatMessagesHistory(userId, AuthorRole.User.ToString(), ask, cancellationToken); // Save in chat history the user message (a.k.a. ask). await SaveChatMessagesHistory(userId, AuthorRole.Assistant.ToString(), response, cancellationToken); // Save in chat history the assistant message (a.k.a. response). @@ -110,7 +110,9 @@ protected virtual async Task GetErrorMessageAsync(ChatHistory chatHistor chatHistory.AddSystemMessage(prompt); - return await chat.GenerateMessageAsync(chatHistory, options.ChatRequestSettings, cancellationToken); + var chatMessage = await kernel.GetRequiredService().GetChatMessageContentAsync(chatHistory, options.ChatRequestSettings, kernel, cancellationToken); + + return chatMessage.Content; } /// @@ -128,7 +130,7 @@ protected virtual async Task LoadChatHistoryAsync(ChatHistory chatHistory, strin { if (options.HistoryMaxMessages <= 0 || remainingTokens <= 0) { - return; + return; } // Obtain the chat history for the user, ordered by timestamps descending to get the most recent messages first, and then take 'N' messages. @@ -145,7 +147,8 @@ protected virtual async Task LoadChatHistoryAsync(ChatHistory chatHistory, strin result.TakeWhile(item => { - var tokensHistoryMessage = tokensLengthFunction(item.Message); + var itemRole = item.RoleName == assistantRoleName ? AuthorRole.Assistant : AuthorRole.User; + var tokensHistoryMessage = GetChatMessageTokenCount(itemRole, item.Message); if (tokensHistoryMessage <= remainingTokens) { @@ -200,4 +203,19 @@ await chatMessagesHistoryRepository.AddAsync(new ChatMessageHistoryRecord() TimestampUtc = DateTime.UtcNow, }, cancellationToken); } + + /// + /// Gets a rough token count of a message for the by following the syntax defined by Azure OpenAI's ChatMessage object. + /// + /// + /// The code of this method is based on . + /// + /// Author role of the message. + /// Content of the message. + /// The calculated token count for the given message. + protected virtual int GetChatMessageTokenCount(AuthorRole authorRole, string content) + { + var tokenCount = authorRole == AuthorRole.System ? tokensLengthFunction("\n") : 0; + return tokenCount + tokensLengthFunction($"role:{authorRole.Label}") + tokensLengthFunction($"content:{content}"); + } } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPluginOptions.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPluginOptions.cs index f7d5476..4d21db7 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPluginOptions.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/Plugins/ChatWithHistoryPluginOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -using Microsoft.SemanticKernel.Connectors.AI.OpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Encamina.Enmarcha.SemanticKernel.Plugins.Chat.Plugins; @@ -17,10 +17,10 @@ public class ChatWithHistoryPluginOptions public virtual int HistoryMaxMessages { get; init; } /// - /// Gets a valid instance of (from Semantic Kernel) with settings for the chat request. + /// Gets a valid instance of (from Semantic Kernel) with settings for the chat request. /// [Required] - public virtual OpenAIRequestSettings ChatRequestSettings { get; init; } = new() + public virtual OpenAIPromptExecutionSettings ChatRequestSettings { get; init; } = new() { MaxTokens = 1000, Temperature = 0.8, diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/README.md b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/README.md index 28b464d..a955d1c 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/README.md +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Chat/README.md @@ -20,7 +20,7 @@ First, [install .NET CLI](https://learn.microsoft.com/en-us/dotnet/core/tools/). ## How to use -To use [ChatWithHistoryPlugin](/Plugins/ChatWithHistoryPlugin.cs), the usual approach is to import it as a plugin within Semantic Kernel. The simplest way to do this is by using the extension method [ImportChatWithHistoryPluginUsingCosmosDb](/IKernelExtensions.cs), which handles the import of the Plugin into Semantic Kernel. However, some previous configuration is required before importing it. +To use [ChatWithHistoryPlugin](/Plugins/ChatWithHistoryPlugin.cs), the usual approach is to import it as a plugin within Semantic Kernel. The simplest way to do this is by using the extension method [ImportChatWithHistoryPluginUsingCosmosDb](/KernelExtensions.cs), which handles the import of the Plugin into Semantic Kernel. However, some previous configuration is required before importing it. First, you need to add the [SemanticKernelOptions](../Encamina.Enmarcha.SemanticKernel.Abstractions/SemanticKernelOptions.cs) and [ChatWithHistoryPluginOptions](./Plugins/ChatWithHistoryPluginOptions.cs) to your project configuration. You can achieve this by using any [configuration provider](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration). The followng code is an example of how the settings should look like using the `appsettings.json` file: ```json @@ -94,9 +94,9 @@ Now you can inject the kernel via constructor, and the chat capabilities are alr ```csharp public class MyClass { - private readonly IKernel kernel; + private readonly Kernel kernel; - public MyClass(IKernel kernel) + public MyClass(Kernel kernel) { this.kernel = kernel; } @@ -127,7 +127,7 @@ You can also inherit from the ChatWithHistoryPlugin class and add the customizat ```csharp public class MyCustomChatWithHistoryPlugin : ChatWithHistoryPlugin { - public MyCustomChatWithHistoryPlugin(IKernel kernel, string chatModelName, Func tokensLengthFunction, IAsyncRepository chatMessagesHistoryRepository, IOptionsMonitor options) + public MyCustomChatWithHistoryPlugin(Kernel kernel, string chatModelName, Func tokensLengthFunction, IAsyncRepository chatMessagesHistoryRepository, IOptionsMonitor options) : base(kernel, chatModelName, tokensLengthFunction, chatMessagesHistoryRepository, options) { } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Encamina.Enmarcha.SemanticKernel.Plugins.Memory.csproj b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Encamina.Enmarcha.SemanticKernel.Plugins.Memory.csproj index 85bff7a..aee5972 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Encamina.Enmarcha.SemanticKernel.Plugins.Memory.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Encamina.Enmarcha.SemanticKernel.Plugins.Memory.csproj @@ -6,8 +6,16 @@ enable + + 1701;1702;SKEXP0003 + + + + 1701;1702;SKEXP0003 + + - + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/IKernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/KernelExtensions.cs similarity index 62% rename from src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/IKernelExtensions.cs rename to src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/KernelExtensions.cs index 6c48348..d4520fd 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/IKernelExtensions.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/KernelExtensions.cs @@ -6,21 +6,21 @@ namespace Encamina.Enmarcha.SemanticKernel.Plugins.Memory; /// -/// Extension methods on to import and configure plugins. +/// Extension methods on to import and configure plugins. /// -public static class IKernelExtensions +public static class KernelExtensions { /// /// Imports the «Memory» plugin and its functions into the kernel. /// - /// The instance to add this plugin. + /// The instance to add this plugin. /// A valid instance of a semantic memory to recall memories associated with text. /// A function to count how many tokens are in a string or text. /// A list of all the functions found in this plugin, indexed by function name. - public static IDictionary ImportMemoryPlugin(this IKernel kernel, ISemanticTextMemory semanticTextMemory, Func tokensLengthFunction) + public static KernelPlugin ImportMemoryPlugin(this Kernel kernel, ISemanticTextMemory semanticTextMemory, Func tokensLengthFunction) { var memoryQueryPlugin = new MemoryQueryPlugin(semanticTextMemory, tokensLengthFunction); - return kernel.ImportFunctions(memoryQueryPlugin, PluginsInfo.MemoryQueryPlugin.Name); + return kernel.ImportPluginFromObject(memoryQueryPlugin, PluginsInfo.MemoryQueryPlugin.Name); } } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Plugins/MemoryQueryPlugin.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Plugins/MemoryQueryPlugin.cs index 43fd439..4aaaf9c 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Plugins/MemoryQueryPlugin.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/Plugins/MemoryQueryPlugin.cs @@ -36,7 +36,7 @@ public MemoryQueryPlugin(ISemanticTextMemory semanticTextMemory, FuncThe character that separates each memory's collection name in . /// The to monitor for cancellation requests. /// A string representing all the information found from searching the memory's collections using the given . - [SKFunction] + [KernelFunction] [Description(@"Searches the memory by looking up for a given query, from a list (usually comma-separated) of in memory's collections .")] public virtual async Task QueryMemoryAsync( [Description(@"The query to search the memory for")] string query, diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/README.md b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/README.md index 1d08acd..84232d9 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/README.md +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Memory/README.md @@ -20,7 +20,7 @@ First, [install .NET CLI](https://learn.microsoft.com/en-us/dotnet/core/tools/). ## How to use -To use [MemoryQueryPlugin](/Plugins/MemoryQueryPlugin.cs), the usual approach is to import it as a plugin within Semantic Kernel. The simplest way to do this is by using the extension method [ImportMemoryPlugin](/IKernelExtensions.cs), which handles the import of the Plugin into Semantic Kernel. +To use [MemoryQueryPlugin](/Plugins/MemoryQueryPlugin.cs), the usual approach is to import it as a plugin within Semantic Kernel. The simplest way to do this is by using the extension method [ImportMemoryPlugin](/KernelExtensions.cs), which handles the import of the Plugin into Semantic Kernel. ```csharp // Entry point @@ -52,9 +52,9 @@ Now you can inject the kernel via constructor, and the memory capabilities are a ```csharp public class MyClass { - private readonly IKernel kernel; + private readonly Kernel kernel; - public MyClass(IKernel kernel) + public MyClass(Kernel kernel) { this.kernel = kernel; } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.csproj b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.csproj index ae79830..d5eb494 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.csproj @@ -6,16 +6,20 @@ enable - - - - + + 1701;1702;SKEXP0003 + + + + 1701;1702;SKEXP0003 + - + + diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/IKernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/IKernelExtensions.cs deleted file mode 100644 index 9801398..0000000 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/IKernelExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Reflection; - -using CommunityToolkit.Diagnostics; - -using Encamina.Enmarcha.SemanticKernel.Abstractions; -using Encamina.Enmarcha.SemanticKernel.Extensions; -using Encamina.Enmarcha.SemanticKernel.Plugins.Memory; -using Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.Plugins; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Memory; - -namespace Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering; - -/// -/// Extension methods on to import and configure plugins. -/// -public static class IKernelExtensions -{ - /// - /// Imports the «Question Answering» plugin and its functions into the kernel. - /// - /// The instance to add this plugin. - /// A to resolve the dependencies. - /// - /// A function to calculate the length by tokens of the chat messages. These functions are usually available in the «mix-in» interface . - /// - /// A list of all the functions found in this plugin, indexed by function name. - /// - public static IDictionary ImportQuestionAnsweringPlugin(this IKernel kernel, IServiceProvider serviceProvider, Func tokensLengthFunction) - { - Guard.IsNotNull(serviceProvider); - Guard.IsNotNull(tokensLengthFunction); - - var semanticKernelOptions = serviceProvider.GetRequiredService>().CurrentValue; - var modelName = semanticKernelOptions.CompletionsModelName ?? semanticKernelOptions.ChatModelName; - - var questionAnsweringPlugin = new QuestionAnsweringPlugin(kernel, modelName, tokensLengthFunction); - - var questionNativeFunc = kernel.ImportFunctions(questionAnsweringPlugin, PluginsInfo.QuestionAnsweringPlugin.Name); - var questionSemanticFunc = kernel.ImportSemanticPluginsFromAssembly(Assembly.GetExecutingAssembly()); - - return questionNativeFunc.Union(questionSemanticFunc).ToDictionary(x => x.Key, x => x.Value); - } - - /// - /// Imports the «Question Answering» plugin and its functions into the kernel also adding the «Memory» plugin. - /// - /// The instance to add this plugin. - /// A to resolve the dependencies. - /// - /// A function to calculate the length by tokens of the chat messages. These functions are usually available in the «mix-in» interface . - /// - /// A list of all the functions found in this plugin, indexed by function name. - /// - public static IDictionary ImportQuestionAnsweringPluginWithMemory(this IKernel kernel, IServiceProvider serviceProvider, Func tokensLengthFunction) - { - var semanticTextMemory = serviceProvider.GetRequiredService(); - - var questionAnsweringFunctions = kernel.ImportQuestionAnsweringPlugin(serviceProvider, tokensLengthFunction); - - var memoryFunc = kernel.ImportMemoryPlugin(semanticTextMemory, tokensLengthFunction); - - return memoryFunc.Union(questionAnsweringFunctions).ToDictionary(x => x.Key, x => x.Value); - } -} diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/KernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/KernelExtensions.cs new file mode 100644 index 0000000..c8d99f8 --- /dev/null +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/KernelExtensions.cs @@ -0,0 +1,56 @@ +using CommunityToolkit.Diagnostics; + +using Encamina.Enmarcha.AI.OpenAI.Abstractions; +using Encamina.Enmarcha.SemanticKernel.Abstractions; +using Encamina.Enmarcha.SemanticKernel.Plugins.Memory; +using Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.Plugins; + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Memory; + +namespace Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering; + +/// +/// Extension methods on to import and configure plugins. +/// +public static class KernelExtensions +{ + /// + /// Imports the «Question Answering» plugin and its functions into the kernel. + /// + /// The instance to add this plugin. + /// Options to connect and use an OpenAI service. + /// + /// A function to calculate the length by tokens of the chat messages. These functions are usually available in the «mix-in» interface . + /// + /// A list of all the functions found in this plugin, indexed by function name. + /// + public static IEnumerable ImportQuestionAnsweringPlugin(this Kernel kernel, OpenAIOptions openAIOptions, Func tokensLengthFunction) + { + Guard.IsNotNull(openAIOptions); + Guard.IsNotNull(tokensLengthFunction); + + kernel.ImportPluginFromObject(new QuestionAnsweringPlugin(kernel, openAIOptions.CompletionsModelName ?? openAIOptions.ChatModelName, tokensLengthFunction), PluginsInfo.QuestionAnsweringPlugin.Name); + + return kernel.Plugins; + } + + /// + /// Imports the «Question Answering» plugin and its functions into the kernel also adding the «Memory» plugin. + /// + /// The instance to add this plugin. + /// Options to connect and use an OpenAI service. + /// A valid instance of to use as memory for this plugin. + /// + /// A function to calculate the length by tokens of the chat messages. These functions are usually available in the «mix-in» interface . + /// + /// A list of all the functions found in this plugin, indexed by function name. + /// + public static IEnumerable ImportQuestionAnsweringPluginWithMemory(this Kernel kernel, OpenAIOptions openAIOptions, ISemanticTextMemory semanticTextMemory, Func tokensLengthFunction) + { + kernel.ImportQuestionAnsweringPlugin(openAIOptions, tokensLengthFunction); + kernel.ImportMemoryPlugin(semanticTextMemory, tokensLengthFunction); + + return kernel.Plugins; + } +} diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin.cs index 7e93ad0..4c459ca 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin.cs @@ -1,12 +1,9 @@ using System.ComponentModel; -using System.Globalization; -using System.Reflection; using Encamina.Enmarcha.AI.OpenAI.Abstractions; -using Encamina.Enmarcha.SemanticKernel.Extensions; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.Plugins; @@ -15,9 +12,36 @@ namespace Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering.Plugins; /// public class QuestionAnsweringPlugin { - private readonly IKernel kernel; + private const string QuestionAnsweringFromContextFunctionPrompt = @" +You ANSWER questions with information from the CONTEXT. +ONLY USE information from CONTEXT +The ANSWER MUST BE ALWAYS in the SAME LANGUAGE as the QUESTION. +If you are unable to find the answer or do not know it, simply say ""I don't know"". +The ""I don't know"" response MUST BE TRANSLATED ALWAYS to the SAME LANGUAGE as the QUESTION. +If presented with a logic question about the CONTEXT, attempt to calculate the answer. +ALWAYS RESPOND with a FINAL ANSWER, DO NOT CONTINUE the conversation. + +[CONTEXT] +{{$context}} + +[QUESTION] +{{$input}} + +[ANSWER] + +"; + + private readonly Kernel kernel; private readonly string modelName; private readonly Func tokenLengthFunction; + private readonly OpenAIPromptExecutionSettings questionAnsweringFromContextFunctionExecutionSettings = new() + { + MaxTokens = 1000, + Temperature = 0.1, + TopP = 1.0, + PresencePenalty = 0.0, + FrequencyPenalty = 0.0, + }; /// /// Initializes a new instance of the class. @@ -25,7 +49,7 @@ public class QuestionAnsweringPlugin /// The instance of the semantic kernel to work with in this plugin. /// The name of the model used by this plugin. /// A function to count how many tokens are in a string or text. - public QuestionAnsweringPlugin(IKernel kernel, string modelName, Func tokensLengthFunction) + public QuestionAnsweringPlugin(Kernel kernel, string modelName, Func tokensLengthFunction) { this.kernel = kernel; this.modelName = modelName; @@ -38,54 +62,43 @@ public QuestionAnsweringPlugin(IKernel kernel, string modelName, Func /// The question to answer and search the memory for. /// A list of collections names, separated by the value of (usually a comma). - /// Maximum number of tokens to use for the response. /// Minimum relevance of the response. /// Maximum number of results from searching each memory's collection. /// The character that separates each memory's collection name in . /// The to monitor for cancellation requests. /// A string representing the answer for the based on all the information found from searching the memory's collections. - [SKFunction] + [KernelFunction] [Description(@"Answer questions using information obtained from a memory. The given question is used as query to search from a list (usually comma-separated) of collections. The result is used as context to answer the question.")] - public virtual async Task QuestionAnsweringFromMemoryQuery( + public virtual async Task QuestionAnsweringFromMemoryQueryAsync( [Description(@"The question to answer and search the memory for")] string question, [Description(@"A list of memory's collections, usually comma-separated")] string collectionsStr, - [Description(@"Available maximum number of tokens for the answer")] int responseTokenLimit, [Description(@"Minimum relevance for the search results")] double minRelevance = 0.75, [Description(@"Maximum number of results per queried collection")] int resultsLimit = 20, [Description(@"The character (usually a comma) that separates each collection from the given list of collections")] char collectionSeparator = ',', CancellationToken cancellationToken = default) { - var modelMaxTokens = ModelInfo.GetById(modelName).MaxTokens; // Get this information early, to throw an exception if the model is not found (fail fast). - // This method was designed to maximize the use of tokens in an LLM model (like GPT). - // First, it calculates the number of tokens in the 'prompt', 'input', and 'output' of the «QuestionAnsweringFromContext» function. (First context) - // Then, uses a new context to call the «QueryMemory» function from the «MemoryQueryPlugin». (Second context) - // Then, it subtracts this value from the total tokens of the model to determine how many tokens can be used for the memory query. (Second context) - // Finally, it injects the result of the memory query into the first context so that the response function can use it. (First context) - - var questionAnsweringVariables = new ContextVariables(); - questionAnsweringVariables.Set(@"input", question); + // Get the number of tokens from the 'prompt', 'input' (question), and execution settings of the «QuestionAnsweringFromContext» function. + // This number of tokens will be subtracted from the total tokens of the used model to determine the limit of tokens allowed for the «QueryMemory» function from the «MemoryQueryPlugin». + // Finally, it uses the result from the «QueryMemory» function as context for the «QuestionAnsweringFromContext» function. The total amount of tokens of this context should be within the + // limits of the available tokens for the context argument of the «QuestionAnsweringFromContext» function. - var questionAnsweringFunction = kernel.Functions.GetFunction(PluginsInfo.QuestionAnsweringPlugin.Name, PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Name); - - // Calculates the number of tokens used in the «QuestionAnsweringFromContext» function. - // This amount will be subtracted from the total tokens of the model to determine the token limit required by the «QueryMemory» function from the «MemoryQueryPlugin». - var questionAnsweringFunctionUsedTokens - = await kernel.GetSemanticFunctionUsedTokensAsync(questionAnsweringFunction, Assembly.GetExecutingAssembly(), questionAnsweringVariables, tokenLengthFunction, cancellationToken); + var modelMaxTokens = ModelInfo.GetById(modelName).MaxTokens; // Get this information early, to throw an exception if the model is not found (fail fast). - // Switches to the context of the memory query function. - var memoryQueryVariables = new ContextVariables(); - memoryQueryVariables.Set(@"query", question); - memoryQueryVariables.Set(@"collectionsStr", collectionsStr); - memoryQueryVariables.Set(@"responseTokenLimit", (modelMaxTokens - questionAnsweringFunctionUsedTokens).ToString(CultureInfo.InvariantCulture)); - memoryQueryVariables.Set(@"minRelevance", minRelevance.ToString(CultureInfo.InvariantCulture)); - memoryQueryVariables.Set(@"resultsLimit", resultsLimit.ToString(CultureInfo.InvariantCulture)); - memoryQueryVariables.Set(@"collectionSeparator", collectionSeparator.ToString(CultureInfo.InvariantCulture)); + var memoryQueryVariables = new KernelArguments() + { + [@"query"] = question, + [@"collectionsStr"] = collectionsStr, + [@"responseTokenLimit"] = modelMaxTokens - QuestionAnsweringFromContextFunctionsUsedTokens(question), + [@"minRelevance"] = minRelevance, + [@"resultsLimit"] = resultsLimit, + [@"collectionSeparator"] = collectionSeparator, + }; - // Executes the «QueryMemory» function from the «MemoryQueryPlugin» - var memoryQueryFunction = kernel.Functions.GetFunction(Memory.PluginsInfo.MemoryQueryPlugin.Name, Memory.PluginsInfo.MemoryQueryPlugin.Functions.QueryMemory.Name); + // Executes the «QueryMemory» function from the «MemoryQueryPlugin». + var memoryQueryFunction = kernel.Plugins[Memory.PluginsInfo.MemoryQueryPlugin.Name][Memory.PluginsInfo.MemoryQueryPlugin.Functions.QueryMemory.Name]; - var memoryQueryFunctionResult = await memoryQueryFunction.InvokeAsync(kernel.CreateNewContext(memoryQueryVariables), null, cancellationToken); + var memoryQueryFunctionResult = await memoryQueryFunction.InvokeAsync(kernel, memoryQueryVariables, cancellationToken); var memoryQueryResult = memoryQueryFunctionResult.GetValue(); @@ -96,9 +109,45 @@ var questionAnsweringFunctionUsedTokens } // Return to the context of the response function and set the result of the memory query. - questionAnsweringVariables.Set(@"context", memoryQueryResult); - var questionAnsweringFunctionResult = await questionAnsweringFunction.InvokeAsync(kernel.CreateNewContext(questionAnsweringVariables), null, cancellationToken); + var questionAnsweringVariables = new KernelArguments() + { + [@"input"] = question, + [@"context"] = memoryQueryResult, + }; + + var questionAnsweringFunctionResult = await kernel.Plugins[PluginsInfo.QuestionAnsweringPlugin.Name][PluginsInfo.QuestionAnsweringPlugin.Functions.QuestionAnsweringFromContext.Name] + .InvokeAsync(kernel, questionAnsweringVariables, cancellationToken); return questionAnsweringFunctionResult.GetValue(); } + + /// + /// Answer questions using information from a given context. + /// + /// The question to answer with information from a context given in . + /// The context with information that may contain the answer for question from . + /// The to monitor for cancellation requests. + /// A string representing the answer for the based on all the information found from searching the memory's collections. + [KernelFunction] + [Description(@"Answer questions using information from a context.")] + public virtual async Task QuestionAnsweringFromContextAsync( + [Description(@"The question to answer with information from a context.")] string input, + [Description(@"Context with information that may contain the answer for question")] string context, + CancellationToken cancellationToken = default) + { + var functionArguments = new KernelArguments(questionAnsweringFromContextFunctionExecutionSettings) + { + [@"input"] = input, + [@"context"] = context, + }; + + var functionResult = await kernel.InvokePromptAsync(QuestionAnsweringFromContextFunctionPrompt, functionArguments, cancellationToken: cancellationToken); + + return functionResult.GetValue(); + } + + private int QuestionAnsweringFromContextFunctionsUsedTokens(string input) + { + return tokenLengthFunction(QuestionAnsweringFromContextFunctionPrompt) + questionAnsweringFromContextFunctionExecutionSettings.MaxTokens.Value + tokenLengthFunction(input); + } } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin/QuestionAnsweringFromContext/config.json b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin/QuestionAnsweringFromContext/config.json deleted file mode 100644 index b2a94aa..0000000 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin/QuestionAnsweringFromContext/config.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "schema": 1, - "type": "completion", - "description": "Answer questions using information from a context.", - "completion": { - "max_tokens": 1000, - "temperature": 0.1, - "top_p": 1.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0 - }, - "input": { - "parameters": [ - { - "name": "input", - "description": "The question to answer with information from a context.", - "defaultValue": "" - }, - { - "name": "context", - "description": "Context with information that may contain the answer for question", - "defaultValue": "" - } - ] - } -} \ No newline at end of file diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin/QuestionAnsweringFromContext/skprompt.txt b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin/QuestionAnsweringFromContext/skprompt.txt deleted file mode 100644 index 6822e03..0000000 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/Plugins/QuestionAnsweringPlugin/QuestionAnsweringFromContext/skprompt.txt +++ /dev/null @@ -1,15 +0,0 @@ -You ANSWER questions with information from the CONTEXT. -ONLY USE information from CONTEXT -The ANSWER MUST BE ALWAYS in the SAME LANGUAGE as the QUESTION. -If you are unable to find the answer or do not know it, simply say "I don't know". -The "I don't know" response MUST BE TRANSLATED ALWAYS to the SAME LANGUAGE as the QUESTION. -If presented with a logic question about the CONTEXT, attempt to calculate the answer. -ALWAYS RESPOND with a FINAL ANSWER, DO NOT CONTINUE the conversation. - -[CONTEXT] -{{$context}} - -[QUESTION] -{{$input}} - -[ANSWER] diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/PluginsInfo.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/PluginsInfo.cs index d034c0a..222743d 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/PluginsInfo.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/PluginsInfo.cs @@ -59,7 +59,7 @@ public static class QuestionAnsweringFromMemoryQuery /// /// The name of the function. /// - public static readonly string Name = nameof(Plugins.QuestionAnsweringPlugin.QuestionAnsweringFromMemoryQuery).RemoveAsyncSuffix(); + public static readonly string Name = nameof(Plugins.QuestionAnsweringPlugin.QuestionAnsweringFromMemoryQueryAsync).RemoveAsyncSuffix(); /// /// Information about the function's parameters. diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/README.md b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/README.md index 82fa8db..5379d7f 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/README.md +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.QuestionAnswering/README.md @@ -57,9 +57,9 @@ Now you can inject the kernel via constructor, and the question capabilities are ```csharp public class MyClass { - private readonly IKernel kernel; + private readonly Kernel kernel; - public MyClass(IKernel kernel) + public MyClass(Kernel kernel) { this.kernel = kernel; } @@ -128,9 +128,9 @@ Now you can inject the kernel via constructor, and the memory question capabilit ```csharp public class MyClass { - private readonly IKernel kernel; + private readonly Kernel kernel; - public MyClass(IKernel kernel) + public MyClass(Kernel kernel) { this.kernel = kernel; } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/IKernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/KernelExtensions.cs similarity index 55% rename from src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/IKernelExtensions.cs rename to src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/KernelExtensions.cs index ea67b7f..e6dd9fa 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/IKernelExtensions.cs +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/KernelExtensions.cs @@ -7,18 +7,18 @@ namespace Encamina.Enmarcha.SemanticKernel.Plugins.Text; /// -/// Extension methods on to import and configure plugins. +/// Extension methods on to import and configure plugins. /// -public static class IKernelExtensions +public static class KernelExtensions { /// /// Imports the «Text» plugin and its functions into the kernel. /// - /// The instance to add this plugin. + /// The instance to add this plugin. /// A list of all the functions found in this plugin, indexed by function name. - public static IDictionary ImportTextPlugin(this IKernel kernel) + public static IEnumerable ImportTextPlugin(this Kernel kernel) { - return kernel.ImportSemanticPluginsFromAssembly(Assembly.GetExecutingAssembly()); + return kernel.ImportPromptFunctionsFromAssembly(Assembly.GetExecutingAssembly()); } } diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/KeyPhrases/config.json b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/KeyPhrases/config.json index 9cc73fe..3b2c651 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/KeyPhrases/config.json +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/KeyPhrases/config.json @@ -1,26 +1,27 @@ { "schema": 1, - "type": "completion", "description": "Extract keyphrases from a given text or any text document", - "completion": { - "max_tokens": 1250, - "temperature": 0.0, - "top_p": 1.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0 + "execution_settings": { + "default": { + "max_tokens": 1250, + "temperature": 0.0, + "top_p": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } }, - "input": { - "parameters": [ - { - "name": "input", - "description": "Text to analyze and extract keyphrases from", - "defaultValue": "" - }, - { - "name": "topKeyphrases", - "description": "Top number of keyphrases to extract", - "defaultValue": "2" - } - ] - } + "input_variables": [ + { + "name": "input", + "description": "Text to analyze and extract keyphrases from", + "default": "", + "is_required": true + }, + { + "name": "topKeyphrases", + "description": "Top number of keyphrases to extract", + "default": "2", + "is_required": false + } + ] } \ No newline at end of file diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/Summarize/config.json b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/Summarize/config.json index 81871db..f8474a9 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/Summarize/config.json +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/Plugins/TextPlugin/Summarize/config.json @@ -1,31 +1,33 @@ { "schema": 1, - "type": "completion", "description": "Summarizes a given input text", - "completion": { - "max_tokens": 1250, - "temperature": 0.1, - "top_p": 1.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0 + "execution_settings": { + "default": { + "max_tokens": 1250, + "temperature": 0.1, + "top_p": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } }, - "input": { - "parameters": [ - { - "name": "input", - "description": "The input text to summarize", - "defaultValue": "" - }, - { - "name": "maxWordsCount", - "description": "Maximum number of words that the summary should have", - "defaultValue": "1000" - }, - { - "name": "locale", - "description": "Language in which the summary will be generated", - "defaultValue": "en" - } - ] - } -} \ No newline at end of file + "input_variables": [ + { + "name": "input", + "description": "The input text to summarize", + "default": "", + "is_required": true + }, + { + "name": "maxWordsCount", + "description": "Maximum number of words that the summary should have", + "default": "1000", + "is_required": false + }, + { + "name": "locale", + "description": "Language in which the summary will be generated", + "default": "en", + "is_required": false + } + ] +} diff --git a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/README.md b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/README.md index b995d1a..57c5318 100644 --- a/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/README.md +++ b/src/Encamina.Enmarcha.SemanticKernel.Plugins.Text/README.md @@ -54,9 +54,9 @@ Now you can inject the kernel via constructor, and the text capabilities are alr ```csharp public class MyClass { - private readonly IKernel kernel; + private readonly Kernel kernel; - public MyClass(IKernel kernel) + public MyClass(Kernel kernel) { this.kernel = kernel; } diff --git a/src/Encamina.Enmarcha.SemanticKernel/Encamina.Enmarcha.SemanticKernel.csproj b/src/Encamina.Enmarcha.SemanticKernel/Encamina.Enmarcha.SemanticKernel.csproj index ba84ef4..12ea7b6 100644 --- a/src/Encamina.Enmarcha.SemanticKernel/Encamina.Enmarcha.SemanticKernel.csproj +++ b/src/Encamina.Enmarcha.SemanticKernel/Encamina.Enmarcha.SemanticKernel.csproj @@ -4,6 +4,14 @@ net6 + + 1701;1702;SKEXP0001;SKEXP0003 + + + + 1701;1702;SKEXP0001;SKEXP0003 + + diff --git a/src/Encamina.Enmarcha.SemanticKernel/Extensions/IKernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel/Extensions/IKernelExtensions.cs deleted file mode 100644 index c32a010..0000000 --- a/src/Encamina.Enmarcha.SemanticKernel/Extensions/IKernelExtensions.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Reflection; -using System.Text.Json; - -using Encamina.Enmarcha.Core.Extensions; - -using Encamina.Enmarcha.SemanticKernel.Extensions.Resources; - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.TemplateEngine; -using Microsoft.SemanticKernel.TemplateEngine.Basic; - -namespace Encamina.Enmarcha.SemanticKernel.Extensions; - -/// -/// Extension methods on . -/// -public static class IKernelExtensions -{ - /// - /// Generates the final prompt for a given semantic function in a directory located plugin, and using the context variables. - /// - /// The to work with. - /// The semantic function representation. - /// The directory containing the plugin and the files that represents and configures the semantic function. - /// A collection of context variables. - /// A cancellation token that can be used to receive notice of cancellation. - /// A string containing the generated prompt. - public static async Task GetSemanticFunctionPromptAsync(this IKernel kernel, ISKFunction skFunction, string functionPluginDirectory, IDictionary contextVariables, CancellationToken cancellationToken) - { - var kernelContext = kernel.CreateNewContext(); - - foreach (var (key, value) in contextVariables) - { - kernelContext.Variables[key] = value; - } - - var promptTemplatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, functionPluginDirectory, skFunction.SkillName, skFunction.Name, Constants.PromptFile); - - return File.Exists(promptTemplatePath) - ? await kernel.PromptTemplateEngine.RenderAsync(await File.ReadAllTextAsync(promptTemplatePath, cancellationToken), kernelContext, cancellationToken) - : throw new FileNotFoundException(ExceptionMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ExceptionMessages.PromptFileNotFound), skFunction.Name, skFunction.SkillName, functionPluginDirectory)); - } - - /// - /// Generates the final prompt for a given semantic function from embedded resources in an assembly, using the context variables. - /// - /// The to work with. - /// The semantic function representation. - /// The assembly containing the embedded resources that represents and configures the semantic function. - /// A collection of context variables. - /// A cancellation token that can be used to receive notice of cancellation. - /// A string containing the generated prompt. - public static async Task GetSemanticFunctionPromptAsync(this IKernel kernel, ISKFunction skFunction, Assembly assembly, IDictionary contextVariables, CancellationToken cancellationToken) - { - var kernelContext = kernel.CreateNewContext(); - - foreach (var (key, value) in contextVariables) - { - kernelContext.Variables[key] = value; - } - - var resourceNames = assembly.GetManifestResourceNames() - .Where(resourceName => resourceName.IndexOf($"{skFunction.SkillName}.{skFunction.Name}", StringComparison.OrdinalIgnoreCase) != -1) - .ToList(); // Enumerate here to improve performance. - - var promptConfigurationResourceName = resourceNames.SingleOrDefault(resourceName => resourceName.EndsWith(Constants.ConfigFile, StringComparison.OrdinalIgnoreCase)); - var promptTemplateResourceName = resourceNames.SingleOrDefault(resourceName => resourceName.EndsWith(Constants.PromptFile, StringComparison.OrdinalIgnoreCase)); - - if (string.IsNullOrEmpty(promptConfigurationResourceName) || string.IsNullOrEmpty(promptTemplateResourceName)) - { - return null; - } - - // TODO : Check this once the final version of Semantic Kernel is released (it seems that it will be changed by `KernelPromptTemplateFactory`)... - var promptTemplateConfig = PromptTemplateConfig.FromJson(await ReadResourceAsync(assembly, promptConfigurationResourceName)); - var promptTemplate = new BasicPromptTemplateFactory(kernel.LoggerFactory).Create(await ReadResourceAsync(assembly, promptTemplateResourceName), promptTemplateConfig); - - return await promptTemplate.RenderAsync(kernelContext, cancellationToken); - } - - /// - /// Calculates the current total number of tokens used in generating a prompt of a given semantic function in a directory located plugin, and using the context variables. - /// - /// The to work with. - /// The semantic function representation. - /// The directory containing the plugin and the files that represents and configures the semantic function. - /// A collection of context variables. - /// A function to calculate length of a string in tokens.. - /// A cancellation token that can be used to receive notice of cancellation. - /// The total number of tokens used plus the maximum allowed response tokens specified in the function. - public static async Task GetSemanticFunctionUsedTokensAsync(this IKernel kernel, ISKFunction skFunction, string functionPluginDirectory, IDictionary contextVariables, Func tokenLengthFunction, CancellationToken cancellationToken) - { - return tokenLengthFunction(await kernel.GetSemanticFunctionPromptAsync(skFunction, functionPluginDirectory, contextVariables, cancellationToken)) + GetMaxTokensFrom(skFunction); - } - - /// - /// Calculates the current total number of tokens used in generating a prompt of a given semantic function from embedded resources in an assembly, using the context variables. - /// - /// The to work with. - /// The semantic function representation. - /// The assembly containing the embedded resources that represents and configures the semantic function. - /// A collection of context variables. - /// A function to calculate length of a string in tokens.. - /// A cancellation token that can be used to receive notice of cancellation. - /// The total number of tokens used plus the maximum allowed response tokens specified in the function. - public static async Task GetSemanticFunctionUsedTokensAsync(this IKernel kernel, ISKFunction skFunction, Assembly assembly, IDictionary contextVariables, Func tokenLengthFunction, CancellationToken cancellationToken) - { - return tokenLengthFunction(await kernel.GetSemanticFunctionPromptAsync(skFunction, assembly, contextVariables, cancellationToken)) + GetMaxTokensFrom(skFunction); - } - - /// - /// Imports plugins with semantic functions from embedded resources in an assembly that represents their prompt and configuration files. - /// - /// The to work with. - /// The assembly containing the embedded resources that represents and configures the semantic function. - /// A list of all the semantic functions found in the assembly, indexed by function name. - public static IDictionary ImportSemanticPluginsFromAssembly(this IKernel kernel, Assembly assembly) - { - var plugins = new Dictionary(); - - var pluginsInfoGroups = assembly.GetManifestResourceNames() - .Where(resourceName => resourceName.EndsWith(Constants.ConfigFile, StringComparison.OrdinalIgnoreCase) || resourceName.EndsWith(Constants.PromptFile, StringComparison.OrdinalIgnoreCase)) - .Select(resourceName => - { - var resourceNameTokens = resourceName.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - // Initially, We do not know in which positions of the embedded resource's name is the name of the plugin and the name of each of its functions. - // We know that the end of the embedded resource's name is either `config.json` or `skprompt.txt`, so we can work backwards from there to find this information. - // So, the last token is the extension (either `.txt` or `.json`), then comes the file name (either `config` or `skprompt`), next is the name of the function, - // and finally the name of the plugin. - // Example: «xxxx.yyyyy.zzzzz.[SkillName].[FunctionName].config.json» and «xxxx.yyyyy.zzzzz.[SkillName].[FunctionName].skprompt.txt» - return new - { - FileName = $@"{resourceNameTokens[^2]}.{resourceNameTokens[^1]}", // The file name and its extension are the last two tokens (first and second position from the end). - FunctionName = resourceNameTokens[^3], // Next always comes the name of the function, which is in the third position from the end. - PluginName = resourceNameTokens[^4], // Finally comes the name of the plugin, which is in the fourth position from the end. - ResourceName = resourceName, - }; - }) - .GroupBy(x => (x.PluginName, x.FunctionName), x => (x.ResourceName, x.FileName)) // Group by skill and function names to get all the resources (prompt and configuration) for each function. - ; - - foreach (var pluginsInfoGroup in pluginsInfoGroups) - { - var functionConfigResourceName = GetResourceNameFromPluginInfoByFileName(pluginsInfoGroup, Constants.ConfigFile); - var functionPromptResourceName = GetResourceNameFromPluginInfoByFileName(pluginsInfoGroup, Constants.PromptFile); - - var promptTemplateConfig = PromptTemplateConfig.FromJson(ReadResource(assembly, functionConfigResourceName)); - - // TODO : Check this once the final version of Semantic Kernel is released (it seems that it will be changed by `KernelPromptTemplateFactory`)... - var promptTemplate = new BasicPromptTemplateFactory(kernel.LoggerFactory).Create(ReadResource(assembly, functionPromptResourceName), promptTemplateConfig); - - var (pluginName, functionName) = pluginsInfoGroup.Key; - - plugins[functionName] = kernel.RegisterSemanticFunction(pluginName, functionName, promptTemplateConfig, promptTemplate); - } - - return plugins; - } - - private static int GetMaxTokensFrom(ISKFunction sKFunction) - { - return sKFunction.RequestSettings.ExtensionData.TryGetValue(@"max_tokens", out var maxTokensObj) && maxTokensObj is JsonElement maxTokensElement && maxTokensElement.TryGetInt32(out var value) ? value : 0; - } - - private static string ReadResource(Assembly assembly, string resourceName) - { - using var stream = assembly.GetManifestResourceStream(resourceName); - using var streamReader = new StreamReader(stream); - - return streamReader.ReadToEnd(); - } - - private static async Task ReadResourceAsync(Assembly assembly, string resourceName) - { - using var stream = assembly.GetManifestResourceStream(resourceName); - using var streamReader = new StreamReader(stream); - - return await streamReader.ReadToEndAsync(); - } - - private static string GetResourceNameFromPluginInfoByFileName(IGrouping<(string PluginName, string FunctionName), (string ResourceName, string FileName)> pluginsInfoGroup, string fileName) - => pluginsInfoGroup.Single(x => fileName.Equals(x.FileName, StringComparison.OrdinalIgnoreCase)).ResourceName; -} diff --git a/src/Encamina.Enmarcha.SemanticKernel/Extensions/IServiceCollectionExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel/Extensions/IServiceCollectionExtensions.cs index 0747324..dd7c83b 100644 --- a/src/Encamina.Enmarcha.SemanticKernel/Extensions/IServiceCollectionExtensions.cs +++ b/src/Encamina.Enmarcha.SemanticKernel/Extensions/IServiceCollectionExtensions.cs @@ -23,11 +23,11 @@ public static class IServiceCollectionExtensions /// The to add services to. /// The so that additional calls can be chained. /// - /// If the does not contain a service reference for the and the services. + /// If the does not contain a service reference for the and the services. /// public static IServiceCollection AddMemoryManager(this IServiceCollection services) { - Guard.IsTrue(services.Any(service => service.ServiceType == typeof(IKernel)), @"Service `IKernel` is required to use `MemoryManager`."); + Guard.IsTrue(services.Any(service => service.ServiceType == typeof(Kernel)), @"Service `Kernel` is required to use `MemoryManager`."); Guard.IsTrue(services.Any(service => service.ServiceType == typeof(IMemoryStore)), @"Service `IMemoryStore` is required to use `MemoryManager`."); services.TryAddScoped(); diff --git a/src/Encamina.Enmarcha.SemanticKernel/Extensions/KernelExtensions.cs b/src/Encamina.Enmarcha.SemanticKernel/Extensions/KernelExtensions.cs new file mode 100644 index 0000000..6e7b6c2 --- /dev/null +++ b/src/Encamina.Enmarcha.SemanticKernel/Extensions/KernelExtensions.cs @@ -0,0 +1,276 @@ +using System.Reflection; +using System.Text.Json; + +using CommunityToolkit.Diagnostics; + +using Encamina.Enmarcha.Core.Extensions; + +using Encamina.Enmarcha.SemanticKernel.Extensions.Resources; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.TextGeneration; + +namespace Encamina.Enmarcha.SemanticKernel.Extensions; + +/// +/// Extension methods on . +/// +public static class KernelExtensions +{ + /// + /// Generates the final prompt for a given prompt function in a directory located plugin, and using the arguments. + /// + /// + /// IMPORTANT: if is not a prompt function, this method will throw exceptions. + /// + /// The to work with. + /// The directory containing the plugin and the files that represents and configures the prompt function. + /// The function for which the prompt is generated. + /// The arguments passed to the function. + /// + /// The template format for the prompt of the function. This must be provided if is not . This parameter is optional. + /// + /// + /// A to interpret the prompt of the function and its configuration into a . This parameter is optional. + /// + /// A cancellation token that can be used to receive notice of cancellation. + /// A string containing the generated prompt. + public static async Task GetKernelFunctionPromptAsync(this Kernel kernel, string pluginDirectory, KernelFunction function, KernelArguments arguments, string promptTemplateFormat = null, IPromptTemplateFactory promptTemplateFactory = null, CancellationToken cancellationToken = default) + { + var promptConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, pluginDirectory, function.Name, Constants.ConfigFile); + + if (!File.Exists(promptConfigPath)) + { + throw new InvalidOperationException(ExceptionMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ExceptionMessages.PromptConfigurationFileNotFound), function.Name, pluginDirectory)); + } + + var promptTemplatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, pluginDirectory, function.Name, Constants.PromptFile); + + if (!File.Exists(promptTemplatePath)) + { + throw new InvalidOperationException(ExceptionMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ExceptionMessages.PromptTemplateFileNotFound), function.Name, pluginDirectory)); + } + + return await InnerGetKernelFunctionPromptAsync( + kernel, + function.Name, + await File.ReadAllTextAsync(promptTemplatePath, cancellationToken), + await File.ReadAllTextAsync(promptConfigPath, cancellationToken), + arguments, + promptTemplateFormat, + promptTemplateFactory, + cancellationToken); + } + + /// + /// Generates the final prompt for a given prompt function from embedded resources in an assembly, using the arguments. + /// + /// + /// IMPORTANT: if is not a prompt function, this method will throw exceptions. + /// + /// The to work with. + /// The name of the plugin associated with the prompt function. + /// The assembly containing the embedded resources that represents and configures the prompt function. + /// The function for which the prompt is generated. + /// The arguments passed to the function. + /// + /// The template format for the prompt of the function. This must be provided if is not . This parameter is optional. + /// + /// + /// A to interpret the prompt of the function and its configuration into a . This parameter is optional. + /// + /// A cancellation token that can be used to receive notice of cancellation. + /// A string containing the generated prompt. + public static async Task GetKernelFunctionPromptAsync(this Kernel kernel, string pluginName, Assembly assembly, KernelFunction function, KernelArguments arguments, string promptTemplateFormat = null, IPromptTemplateFactory promptTemplateFactory = null, CancellationToken cancellationToken = default) + { + Guard.IsNotNullOrWhiteSpace(pluginName); + + var resourceNames = assembly.GetManifestResourceNames() + .Where(resourceName => resourceName.Contains($"{pluginName}.{function.Name}", StringComparison.OrdinalIgnoreCase)) + .ToList(); // Enumerate here to improve performance. + + var promptConfigurationResourceName = resourceNames.SingleOrDefault(resourceName => resourceName.EndsWith(Constants.ConfigFile, StringComparison.OrdinalIgnoreCase)); + var promptTemplateResourceName = resourceNames.SingleOrDefault(resourceName => resourceName.EndsWith(Constants.PromptFile, StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrEmpty(promptConfigurationResourceName) || string.IsNullOrEmpty(promptTemplateResourceName)) + { + throw new InvalidOperationException(ExceptionMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ExceptionMessages.PromptEmbeddedResourcesNotFound), pluginName, function.Name, assembly.GetName().Name)); + } + + return await InnerGetKernelFunctionPromptAsync( + kernel, + function.Name, + ReadResource(assembly, promptTemplateResourceName), + ReadResource(assembly, promptConfigurationResourceName), + arguments, + promptTemplateFormat, + promptTemplateFactory, + cancellationToken); + } + + /// + /// Calculates the current total number of tokens used in generating a prompt of a given prompt function in a directory located plugin, and using the arguments. + /// + /// The to work with. + /// The directory containing the plugin and the files that represents and configures the prompt function. + /// The function for which tokens are calculated. + /// The arguments passed to the function. + /// A function to calculate length of a string in tokens. + /// + /// The template format for the prompt of the function. This must be provided if is not . This parameter is optional. + /// + /// + /// A to interpret the prompt of the function and its configuration into a . This parameter is optional. + /// + /// A cancellation token that can be used to receive notice of cancellation. + /// The total number of tokens used plus the maximum allowed response tokens specified in the function. + public static async Task GetKernelFunctionUsedTokensAsync(this Kernel kernel, string pluginDirectory, KernelFunction function, KernelArguments arguments, Func tokenLengthFunction, string promptTemplateFormat = null, IPromptTemplateFactory promptTemplateFactory = null, CancellationToken cancellationToken = default) + { + return tokenLengthFunction(await kernel.GetKernelFunctionPromptAsync(pluginDirectory, function, arguments, promptTemplateFormat, promptTemplateFactory, cancellationToken)) + + GetMaxTokensFromKernelFunction(kernel, function, arguments); + } + + /// + /// Calculates the current total number of tokens used in generating a prompt of a given prompt function in a directory located plugin, and using the arguments. + /// + /// The to work with. + /// The name of the plugin associated with the prompt function. + /// The assembly containing the embedded resources that represents and configures the prompt function. + /// The function for which tokens are calculated. + /// The arguments passed to the function. + /// A function to calculate length of a string in tokens. + /// + /// The template format for the prompt of the function. This must be provided if is not . This parameter is optional. + /// + /// + /// A to interpret the prompt of the function and its configuration into a . This parameter is optional. + /// + /// A cancellation token that can be used to receive notice of cancellation. + /// The total number of tokens used plus the maximum allowed response tokens specified in the function. + public static async Task GetKernelFunctionUsedTokensAsync(this Kernel kernel, string pluginName, Assembly assembly, KernelFunction function, KernelArguments arguments, Func tokenLengthFunction, string promptTemplateFormat = null, IPromptTemplateFactory promptTemplateFactory = null, CancellationToken cancellationToken = default) + { + return tokenLengthFunction(await kernel.GetKernelFunctionPromptAsync(pluginName, assembly, function, arguments, promptTemplateFormat, promptTemplateFactory, cancellationToken)) + + GetMaxTokensFromKernelFunction(kernel, function, arguments); + } + + /// + /// Imports plugins with prompt functions from embedded resources in an assembly that represents their prompt and configuration files. + /// + /// The to work with. + /// The assembly containing the embedded resources that represents and configures the prompt function. + /// + /// The to use when interpreting discovered prompts into s. + /// If , a default factory will be used. + /// + /// The to use for logging. If null, no logging will be performed. + /// A list of all the semantic functions found in the assembly, indexed by function name. + public static IEnumerable ImportPromptFunctionsFromAssembly(this Kernel kernel, Assembly assembly, IPromptTemplateFactory promptTemplateFactory = null, ILoggerFactory loggerFactory = null) + { + var plugins = new List(); + + var pluginsInfoGroups = assembly.GetManifestResourceNames() + .Where(resourceName => resourceName.EndsWith(Constants.ConfigFile, StringComparison.OrdinalIgnoreCase) || resourceName.EndsWith(Constants.PromptFile, StringComparison.OrdinalIgnoreCase)) + .Select(resourceName => + { + var resourceNameTokens = resourceName.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // Initially, We do not know in which positions of the embedded resource's name is the name of the plugin and the name of each of its functions. + // We know that the end of the embedded resource's name is either `config.json` or `skprompt.txt`, so we can work backwards from there to find this information. + // So, the last token is the extension (either `.txt` or `.json`), then comes the file name (either `config` or `skprompt`), next is the name of the function, + // and finally the name of the plugin. + // Example: «xxxx.yyyyy.zzzzz.[SkillName].[FunctionName].config.json» and «xxxx.yyyyy.zzzzz.[SkillName].[FunctionName].skprompt.txt» + return new + { + FileName = $@"{resourceNameTokens[^2]}.{resourceNameTokens[^1]}", // The file name and its extension are the last two tokens (first and second position from the end). + FunctionName = resourceNameTokens[^3], // Next always comes the name of the function, which is in the third position from the end. + PluginName = resourceNameTokens[^4], // Finally comes the name of the plugin, which is in the fourth position from the end. + ResourceName = resourceName, + }; + }) + .GroupBy(x => (x.PluginName, x.FunctionName), x => (x.ResourceName, x.FileName)) // Group by skill and function names to get all the resources (prompt and configuration) for each function in a plugin. + .GroupBy(x => x.Key.PluginName, x => x) // Then, group by plugin name to get all the functions in that plugin. + ; + + var innerLoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + + var factory = promptTemplateFactory ?? new KernelPromptTemplateFactory(innerLoggerFactory ?? NullLoggerFactory.Instance); + + foreach (var pluginsInfoGroup in pluginsInfoGroups) + { + var functions = new List(); + + foreach (var functionInfoGroup in pluginsInfoGroup) + { + var functionConfigResourceName = GetResourceNameFromPluginInfoByFileName(functionInfoGroup, Constants.ConfigFile); + var functionPromptResourceName = GetResourceNameFromPluginInfoByFileName(functionInfoGroup, Constants.PromptFile); + + var promptConfig = PromptTemplateConfig.FromJson(ReadResource(assembly, functionConfigResourceName)); + promptConfig.Name = functionInfoGroup.Key.FunctionName; + promptConfig.Template = ReadResource(assembly, functionPromptResourceName); + + var promptTemplateInstance = factory.Create(promptConfig); + + functions.Add(KernelFunctionFactory.CreateFromPrompt(promptTemplateInstance, promptConfig, innerLoggerFactory)); + } + + plugins.Add(KernelPluginFactory.CreateFromFunctions(pluginsInfoGroup.Key, functions)); + } + + kernel.Plugins.AddRange(plugins); + + return plugins; + } + + private static int GetMaxTokensFromKernelFunction(Kernel kernel, KernelFunction function, KernelArguments arguments) + { + // Try to use IChatCompletionService as the IAService to retrieve the service settings, but fallback to ITextGenerationService if it's not available. + // Once the service settings are retrieved, get the value of the `max_tokens` property from the extension data. + // If the `max_tokens` property is found, check is a JsonElement and try to get its value as an integer. + // Finally, if the value is an integer, return it. + // In any other case (if the service settings are not found, or the `max_tokens` property is not found, or the value is not an integer), throw an exception. + return (kernel.ServiceSelector.TrySelectAIService(kernel, function, arguments, out _, out var serviceSettings) || kernel.ServiceSelector.TrySelectAIService(kernel, function, arguments, out _, out serviceSettings)) + && serviceSettings?.ExtensionData is not null + && serviceSettings.ExtensionData.TryGetValue(@"max_tokens", out var maxTokensObj) + && maxTokensObj is JsonElement maxTokensElement + && maxTokensElement.TryGetInt32(out var value) + ? value + : 0; + } + + private static string GetResourceNameFromPluginInfoByFileName(IGrouping<(string PluginName, string FunctionName), (string ResourceName, string FileName)> pluginsInfoGroup, string fileName) + => pluginsInfoGroup.Single(x => fileName.Equals(x.FileName, StringComparison.OrdinalIgnoreCase)).ResourceName; + + private static async Task InnerGetKernelFunctionPromptAsync(Kernel kernel, string functionName, string promptTemplate, string promptConfigJsonString, KernelArguments arguments, string templateFormat = null, IPromptTemplateFactory promptTemplateFactory = null, CancellationToken cancellationToken = default) + { + if (promptTemplateFactory is not null && string.IsNullOrWhiteSpace(templateFormat)) + { + throw new ArgumentException($"Template format is required when providing a `{nameof(promptTemplateFactory)}`!", nameof(templateFormat)); + } + + var promptConfig = PromptTemplateConfig.FromJson(promptConfigJsonString); + promptConfig.Name = functionName; + promptConfig.Template = promptTemplate; + promptConfig.TemplateFormat = templateFormat ?? PromptTemplateConfig.SemanticKernelTemplateFormat; + + if (arguments.ExecutionSettings is not null) + { + promptConfig.ExecutionSettings = arguments.ExecutionSettings.ToDictionary(x => x.Key, x => x.Value); + } + + var factory = promptTemplateFactory ?? new KernelPromptTemplateFactory(NullLoggerFactory.Instance); + + return await factory.Create(promptConfig).RenderAsync(kernel, arguments, cancellationToken); + } + + private static string ReadResource(Assembly assembly, string resourceName) + { + using var stream = assembly.GetManifestResourceStream(resourceName); + using var streamReader = new StreamReader(stream); + + return streamReader.ReadToEnd(); + } +} diff --git a/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.Designer.cs b/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.Designer.cs index 49f68ad..99c4a22 100644 --- a/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.Designer.cs +++ b/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.Designer.cs @@ -61,11 +61,29 @@ internal ExceptionMessages() { } /// - /// Looks up a localized string similar to Prompt file ('skprompt.txt') for function '{function}' from plug-in '{plugin} not found at '{templatePath}'!. + /// Looks up a localized string similar to Prompt configuration file ('config.json') for function '{0}' not found at '{1}'!. /// - internal static string PromptFileNotFound { + internal static string PromptConfigurationFileNotFound { get { - return ResourceManager.GetString("PromptFileNotFound", resourceCulture); + return ResourceManager.GetString("PromptConfigurationFileNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prompt embedded resources (template and configuration files) not found for plugin name '{0}' and function '{1}' in assembly '{2}'!. + /// + internal static string PromptEmbeddedResourcesNotFound { + get { + return ResourceManager.GetString("PromptEmbeddedResourcesNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prompt template file ('skprompt.txt') for function '{0}' not found at '{1}'!. + /// + internal static string PromptTemplateFileNotFound { + get { + return ResourceManager.GetString("PromptTemplateFileNotFound", resourceCulture); } } } diff --git a/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.resx b/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.resx index 3426895..57b9f72 100644 --- a/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.resx +++ b/src/Encamina.Enmarcha.SemanticKernel/Extensions/Resources/ExceptionMessages.resx @@ -117,7 +117,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Prompt file ('skprompt.txt') for function '{function}' from plug-in '{plugin} not found at '{templatePath}'! + + Prompt configuration file ('config.json') for function '{0}' not found at '{1}'! + + + Prompt embedded resources (template and configuration files) not found for plugin name '{0}' and function '{1}' in assembly '{2}'! + + + Prompt template file ('skprompt.txt') for function '{0}' not found at '{1}'! \ No newline at end of file diff --git a/src/Encamina.Enmarcha.SemanticKernel/MemoryManager.cs b/src/Encamina.Enmarcha.SemanticKernel/MemoryManager.cs index 42f7817..a5d0780 100644 --- a/src/Encamina.Enmarcha.SemanticKernel/MemoryManager.cs +++ b/src/Encamina.Enmarcha.SemanticKernel/MemoryManager.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Memory; namespace Encamina.Enmarcha.SemanticKernel; @@ -16,28 +16,21 @@ namespace Encamina.Enmarcha.SemanticKernel; /// /// Manager that provides some CRUD operations over memories with multiple chunks that need to be managed by an , using batch operations. /// -public class MemoryManager : IMemoryManager +/// +/// Initializes a new instance of the class. +/// +/// +/// A valid instance of , used to get the configured text embeddings generation service () required by this manager. +/// +/// A valid instance of a to manage. +public class MemoryManager(Kernel kernel, IMemoryStore memoryStore) : IMemoryManager { private const string ChunkSize = @"chunkSize"; - private readonly ILogger logger; - private readonly IMemoryStore memoryStore; - private readonly ITextEmbeddingGeneration textEmbeddingGeneration; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A valid instance of , used to get the configured text embeddings generation service () required by this manager. - /// - /// A valid instance of a to manage. - public MemoryManager(IKernel kernel, IMemoryStore memoryStore) - { - textEmbeddingGeneration = kernel.GetService(); // If the service is not configured, a `KernelException` is thrown... + private readonly Kernel kernel = kernel; - this.logger = kernel.LoggerFactory.CreateLogger(); - this.memoryStore = memoryStore; - } + private readonly ILogger logger = kernel.LoggerFactory.CreateLogger(); + private readonly IMemoryStore memoryStore = memoryStore; /// public virtual async Task UpsertMemoryAsync(string memoryId, string collectionName, IEnumerable chunks, CancellationToken cancellationToken, IDictionary metadata = null) @@ -101,7 +94,7 @@ public virtual async IAsyncEnumerable BatchUpsertMemoriesAsync(string co for (var i = 0; i < totalChunks; i++) { var chunk = memoryContent.Chunks.ElementAt(i); - var embedding = await textEmbeddingGeneration.GenerateEmbeddingAsync(chunk, cancellationToken); + var embedding = await kernel.GetRequiredService().GenerateEmbeddingAsync(chunk, kernel, cancellationToken); memoryRecords.Add(MemoryRecord.LocalRecord($@"{memoryContentId}-{i}", chunk, null, embedding, JsonSerializer.Serialize(memoryContent.Metadata))); } } @@ -152,11 +145,10 @@ private async Task SaveChunks(string memoryid, string collectionName, IEnumerabl for (var i = 0; i < chunksCount; i++) { var chunk = chunks.ElementAt(i); - var embedding = await textEmbeddingGeneration.GenerateEmbeddingAsync(chunk, cancellationToken); + var embedding = await kernel.GetRequiredService().GenerateEmbeddingAsync(chunk, kernel, cancellationToken); memoryRecords.Add(MemoryRecord.LocalRecord(BuildMemoryIdentifier(memoryid, i), chunk, null, embedding, metadataJson)); } await memoryStore.UpsertBatchAsync(collectionName, memoryRecords, cancellationToken).ToListAsync(cancellationToken: cancellationToken); } } - diff --git a/src/Encamina.Enmarcha.SemanticKernel/README.md b/src/Encamina.Enmarcha.SemanticKernel/README.md index 2749036..4f9ccd4 100644 --- a/src/Encamina.Enmarcha.SemanticKernel/README.md +++ b/src/Encamina.Enmarcha.SemanticKernel/README.md @@ -134,9 +134,9 @@ public class MyClass This allows the my-collection collection stored in memory to be tracked, and after [EphemeralMemoryStoreHandlerOptions.IdleTimeoutMinutes](./Options/EphemeralMemoryStoreHandlerOptions.cs) since the last access to the collection through the `GetCollectionNameAsync` method, it will be deleted. -### IKernelExtensions +### KernelExtensions -Contains extension methods for IKernel. You can see all available extension methods in the [IKernelExtensions](./Extensions/IKernelExtensions.cs) class. +Contains extension methods for Kernel. You can see all available extension methods in the [KernelExtensions](./Extensions/KernelExtensions.cs) class. ```csharp var mySemanticFunction = kernel.Skills.GetFunction("MyFunctionName"); diff --git a/src/Encamina.Enmarcha.Testing/Encamina.Enmarcha.Testing.csproj b/src/Encamina.Enmarcha.Testing/Encamina.Enmarcha.Testing.csproj index 9cb9767..7fa7d4e 100644 --- a/src/Encamina.Enmarcha.Testing/Encamina.Enmarcha.Testing.csproj +++ b/src/Encamina.Enmarcha.Testing/Encamina.Enmarcha.Testing.csproj @@ -5,7 +5,7 @@ - + diff --git a/tst/Directory.Build.targets b/tst/Directory.Build.targets index f05ebb3..f252eab 100644 --- a/tst/Directory.Build.targets +++ b/tst/Directory.Build.targets @@ -13,15 +13,11 @@ all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/Encamina.Enmarcha.SemanticKernel.Tests.csproj b/tst/Encamina.Enmarcha.SemanticKernel.Tests/Encamina.Enmarcha.SemanticKernel.Tests.csproj new file mode 100644 index 0000000..f19a1bd --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/Encamina.Enmarcha.SemanticKernel.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + + + + + + + + + + Never + + + Never + + + + + + + + + + Always + + + Always + + + + diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/FixturesCollection.cs b/tst/Encamina.Enmarcha.SemanticKernel.Tests/FixturesCollection.cs new file mode 100644 index 0000000..e3c4695 --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/FixturesCollection.cs @@ -0,0 +1,30 @@ +namespace Encamina.Enmarcha.Testing; + +/// +/// +/// Used to decorate test classes and collections to indicate a test which has per-test-collection fixture data. +/// +/// +/// An instance of the fixture data is initialized just before the first test in the collection is run, and if +/// it implements , it will be disposed after the last test in the collection is run. +/// +/// +/// To gain access to the fixture data from inside the test, a constructor argument should be added to the test +/// class matching exactly matches the TFixture type parameter from any of the . +/// referenced in this collection. +/// +/// +/// +/// Fixture classes can be shared across assemblies, but fixture collection definitions must be in the same assembly +/// as the test that uses them, othertwise they will not work. +/// +[CollectionDefinition(MagicStrings.FixturesCollection)] +public sealed class FixturesCollection + : ICollectionFixture +{ + /*************************************************************************/ + /* This class has no code, and is never created. Its purpose is simply */ + /* to be the place to apply [CollectionDefinition] and all the */ + /* ICollectionFixture<> interfaces. */ + /*************************************************************************/ +} \ No newline at end of file diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/KernelExtensionsTests.cs b/tst/Encamina.Enmarcha.SemanticKernel.Tests/KernelExtensionsTests.cs new file mode 100644 index 0000000..c170152 --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/KernelExtensionsTests.cs @@ -0,0 +1,132 @@ +using System.Reflection; + +using Encamina.Enmarcha.AI.Abstractions; +using Encamina.Enmarcha.SemanticKernel.Extensions; +using Encamina.Enmarcha.Testing; + +using Microsoft.SemanticKernel; + +namespace Encamina.Enmarcha.SemanticKernel.Tests; + +[Collection(MagicStrings.FixturesCollection)] +public class KernelExtensionsTests : FakerProviderFixturedBase +{ + private const string PluginName = "PluginTest"; + private const string PluginDirectory = "TestUtilities"; + + public KernelExtensionsTests(FakerProvider fakerFixture) : base(fakerFixture) + { + } + + [Fact] + public void ImportPromptFunctionsFromAssembly_Succeeds() + { + // Arrange... + var kernel = GivenAKernel(); + + // Act... + var kernelPlugin = kernel.ImportPromptFunctionsFromAssembly(Assembly.GetExecutingAssembly()); + + // Assert.. + var plugin = Assert.Single(kernelPlugin); + Assert.Equal(PluginName, plugin.Name); + + var function = Assert.Single(plugin.ToList()); + Assert.Equal("DummyEmbedded", function.Name); + } + + [Fact] + public async Task GetKernelFunctionPromptAsync_FromPluginDirectory_Succeeds() + { + // Arrange... + var kernel = GivenAKernel(); + kernel.ImportPluginFromPromptDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PluginDirectory, PluginName)); + var function = kernel.Plugins.GetFunction(PluginName, "Dummy"); + var arguments = new KernelArguments() + { + ["input"] = "dummy", + ["foo"] = "foo value", + ["bar"] = "bar value" + }; + + // Act... + var prompt = await kernel.GetKernelFunctionPromptAsync(Path.Combine(PluginDirectory, PluginName), function, arguments); + + // Assert.. + Assert.Equal("This is a Prompt function for testing purposes dummy foo value bar value", prompt); + } + + [Fact] + public async Task GetKernelFunctionPromptAsync_FromAssembly_Succeeds() + { + // Arrange... + var kernel = GivenAKernel(); + kernel.ImportPromptFunctionsFromAssembly(Assembly.GetExecutingAssembly()); + var function = kernel.Plugins.GetFunction(PluginName, "DummyEmbedded"); + var arguments = new KernelArguments() + { + ["input"] = "dummy", + ["foo"] = "foo value", + ["bar"] = "bar value" + }; + + // Act... + var prompt = await kernel.GetKernelFunctionPromptAsync(PluginName, Assembly.GetExecutingAssembly(), function, arguments); + + // Assert.. + Assert.Equal("This is a Prompt function for testing purposes dummy foo value bar value", prompt); + } + + [Fact] + public async Task GetKernelFunctionUsedTokensAsync_FromPluginDirectory_Succeeds() + { + // Arrange... + var kernel = GivenAKernel(); + kernel.ImportPluginFromPromptDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PluginDirectory, PluginName)); + var function = kernel.Plugins.GetFunction(PluginName, "Dummy"); + var arguments = new KernelArguments() + { + ["input"] = "dummy", + ["foo"] = "foo value", + ["bar"] = "bar value" + }; + + // Act... + var usedTokens = await kernel.GetKernelFunctionUsedTokensAsync(Path.Combine(PluginDirectory, PluginName), function, arguments, ILengthFunctions.LengthByCharacterCount); + + //Assert + var expectedUsedTokens = "This is a Prompt function for testing purposes dummy foo value bar value".Length + 500; // prompt with arguments + config json max tokens + Assert.Equal(expectedUsedTokens, usedTokens); + } + + [Fact] + public async Task GetKernelFunctionUsedTokensAsync_FromAssembly_Succeeds() + { + // Arrange... + var kernel = GivenAKernel(); + kernel.ImportPromptFunctionsFromAssembly(Assembly.GetExecutingAssembly()); + var function = kernel.Plugins.GetFunction(PluginName, "DummyEmbedded"); + var arguments = new KernelArguments() + { + ["input"] = "dummy", + ["foo"] = "foo value", + ["bar"] = "bar value" + }; + + // Act... + var usedTokens = await kernel.GetKernelFunctionUsedTokensAsync(PluginName, Assembly.GetExecutingAssembly(), function, arguments, ILengthFunctions.LengthByCharacterCount); + + //Assert + var expectedUsedTokens = "This is a Prompt function for testing purposes dummy foo value bar value".Length + 500; // prompt with arguments + config json max tokens + Assert.Equal(expectedUsedTokens, usedTokens); + } + + private Kernel GivenAKernel() + { + return Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(FakerProvider.GetFaker().Random.Word(), + FakerProvider.GetFaker().Internet.Url(), + FakerProvider.GetFaker().Random.Guid().ToString()) + .Build(); + } +} \ No newline at end of file diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/Dummy/config.json b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/Dummy/config.json new file mode 100644 index 0000000..f6d7ea9 --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/Dummy/config.json @@ -0,0 +1,33 @@ +{ + "schema": 1, + "description": "Dummy description", + "execution_settings": { + "default": { + "max_tokens": 500, + "temperature": 0.0, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } + }, + "input_variables": [ + { + "name": "input", + "description": "The input value.", + "default": "", + "is_required": true + }, + { + "name": "foo", + "description": "The foo value.", + "default": "", + "is_required": false + }, + { + "name": "bar", + "description": "The bar value.", + "default": "", + "is_required": true + } + ] +} \ No newline at end of file diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/Dummy/skprompt.txt b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/Dummy/skprompt.txt new file mode 100644 index 0000000..24cd1a5 --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/Dummy/skprompt.txt @@ -0,0 +1 @@ +This is a Prompt function for testing purposes {{$input}} {{$foo}} {{$bar}} \ No newline at end of file diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyEmbedded/config.json b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyEmbedded/config.json new file mode 100644 index 0000000..f6d7ea9 --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyEmbedded/config.json @@ -0,0 +1,33 @@ +{ + "schema": 1, + "description": "Dummy description", + "execution_settings": { + "default": { + "max_tokens": 500, + "temperature": 0.0, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } + }, + "input_variables": [ + { + "name": "input", + "description": "The input value.", + "default": "", + "is_required": true + }, + { + "name": "foo", + "description": "The foo value.", + "default": "", + "is_required": false + }, + { + "name": "bar", + "description": "The bar value.", + "default": "", + "is_required": true + } + ] +} \ No newline at end of file diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyEmbedded/skprompt.txt b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyEmbedded/skprompt.txt new file mode 100644 index 0000000..24cd1a5 --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyEmbedded/skprompt.txt @@ -0,0 +1 @@ +This is a Prompt function for testing purposes {{$input}} {{$foo}} {{$bar}} \ No newline at end of file diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyNative.cs b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyNative.cs new file mode 100644 index 0000000..4566c0e --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/TestUtilities/PluginTest/DummyNative.cs @@ -0,0 +1,12 @@ +using Microsoft.SemanticKernel; + +namespace Encamina.Enmarcha.SemanticKernel.Tests.TestUtilities.PluginTest; + +public class DummyNative +{ + [KernelFunction] + public string SayHello() + { + return "Hello world"; + } +} diff --git a/tst/Encamina.Enmarcha.SemanticKernel.Tests/Usings.cs b/tst/Encamina.Enmarcha.SemanticKernel.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tst/Encamina.Enmarcha.SemanticKernel.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file