From d70bd6b3aeff651214f036435175449128d68bd3 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:37:48 +0100 Subject: [PATCH 01/24] .Net Remove SKCancelEventArgs unneeded finalizer (#3249) ### Motivation and Context Garba Collection will handle this scenario normally ### Description Removal of unneeded implementation. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- .../Events/SKCancelEventArgs.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Events/SKCancelEventArgs.cs b/dotnet/src/SemanticKernel.Abstractions/Events/SKCancelEventArgs.cs index dfcaae8c14..a3cca0bd5f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Events/SKCancelEventArgs.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Events/SKCancelEventArgs.cs @@ -29,12 +29,4 @@ public abstract class SKCancelEventArgs : SKEventArgs { this._cancelTokenSource.Cancel(); } - - /// - /// Dispose resources. - /// - ~SKCancelEventArgs() - { - this._cancelTokenSource.Dispose(); - } } From da2ea9c83824ade0baf3e5cf2c264a824a5ad1eb Mon Sep 17 00:00:00 2001 From: Abby Harrison <54643756+awharrison-28@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:24:16 -0700 Subject: [PATCH 02/24] Python: Update pyproject.toml to 0.3.14.dev (#3262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update version for next pip package release ### Motivation and Context ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: Co-authored-by: Matthew Bolaños --- python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 4970cf6746..27a7d942d4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "semantic-kernel" -version = "0.3.13.dev" +version = "0.3.14.dev" description = "" authors = ["Microsoft "] readme = "pip/README.md" From aef2bddf8661f7204649d994e321b05c66f5c33b Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:37:00 +0100 Subject: [PATCH 03/24] Revert "Add Java to the CodeQL language matrix" (#3263) Reverts microsoft/semantic-kernel#3257 --- .github/workflows/codeql-analysis.yml | 61 ++++++++++++++------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b4708221f5..a3f6d583cd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,9 +6,9 @@ name: "CodeQL" on: push: - branches: ["main", "experimental*", "feature*"] + branches: [ "main", "experimental*", "feature*" ] schedule: - - cron: "17 11 * * 2" + - cron: '17 11 * * 2' jobs: analyze: @@ -22,44 +22,45 @@ jobs: strategy: fail-fast: false matrix: - language: ["csharp", "python", "java"] + language: [ 'csharp', 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 360102243b8ab8d8a7790408e31872f9c8620b77 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:14:11 +0100 Subject: [PATCH 04/24] .Net: Introduce IAIServiceSelector interface to allow AI service and request settings to be selected during semantic function execution (#3227) ### Motivation and Context Fix following issues for Semantic functions: 1. Allow semantic functions to dynamically retrieve the AI service and request settings to be used when executing the function Previously semantic functions execution has the following limitations: 1. The AI service was set on the semantic function when it was created. This meant that an instance of a semantic function could not be used with multiple different Kernel instances. 1. The AI request settings were set on the semantic function when it was created. This meant that different request settings could not be selected when the function was executed. ### Description This PR add's a new abstraction called `IAIServiceSelector`. An instance of this is added to a semantic function when it is created. The default implementation works as follows: 1. If the semantic function only has a single associated model request setting instance with no service id then the default AI service and the model request settings are used. This is consistent with the previous behaviour. 1. If the semantic function only has a single associated model request setting instance with a service id then the named AI service and the model request settings are used. If the named AI service is not available an exception will be thrown. This is consistent with the previous behaviour. 1. If the semantic function only has multiple associated model request setting instances then: 1. The model request setting instances are considered in order 1. The first model request setting instance with no service id is considered the default 1. For each model request setting instance that has a service id, if the service exists then it will be used with the associated model request settings 1. If no matching service can be found and default request settings are provided the default service will be used 1. If no matching service can be found and no default request settings are provided an exception will be thrown ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- .../Prompt/Blocks/CodeBlockTests.cs | 24 +- .../Prompt/PromptTemplateEngineTests.cs | 8 +- .../Plugins/SamplePluginsTests.cs | 4 - .../Action/ActionPlannerTests.cs | 6 +- ...adOnlyFunctionCollectionExtensionsTests.cs | 13 +- .../Sequential/SequentialPlanParserTests.cs | 12 +- .../Sequential/SequentialPlannerTests.cs | 9 +- .../Functions/IAIServiceSelector.cs | 25 ++ .../Functions/ISKFunction.cs | 17 +- .../Orchestration/SKContext.cs | 10 + .../KernelSemanticFunctionExtensions.cs | 22 +- .../Extensions/SKFunctionExtensions.cs | 4 +- .../Functions/DelegatingAIServiceSelector.cs | 25 ++ .../Functions/InstrumentedSKFunction.cs | 25 +- .../Functions/NativeFunction.cs | 37 +-- .../Functions/OrderedIAIServiceSelector.cs | 61 +++++ .../Functions/SemanticFunction.cs | 81 ++++-- dotnet/src/SemanticKernel.Core/Kernel.cs | 1 + .../Planning/InstrumentedPlan.cs | 25 +- .../src/SemanticKernel.Core/Planning/Plan.cs | 36 +-- .../Functions/FunctionCollectionTests.cs | 6 +- .../Functions/MultipleModelTests.cs | 168 +++++++++++++ ...redIAIServiceConfigurationProviderTests.cs | 232 ++++++++++++++++++ .../Functions/SKContextTests.cs | 14 +- .../Functions/SKFunctionTests2.cs | 3 + .../Functions/SemanticFunctionTests.cs | 32 +-- .../Orchestration/FunctionResultTests.cs | 4 +- .../Orchestration/KernelResultTests.cs | 4 +- .../Planning/PlanSerializationTests.cs | 36 +-- .../Planning/PlanTests.cs | 89 +++---- 30 files changed, 814 insertions(+), 219 deletions(-) create mode 100644 dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs create mode 100644 dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs create mode 100644 dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs diff --git a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs index c6a35e87ff..8004cedc61 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TemplateEngine.Basic.Blocks; using Moq; using Xunit; @@ -21,6 +22,7 @@ public class CodeBlockTests private readonly Mock _functions; private readonly ILoggerFactory _logger = NullLoggerFactory.Instance; private readonly Mock _functionRunner = new(); + private readonly Mock _serviceProvider = new(); public CodeBlockTests() { @@ -32,7 +34,7 @@ public class CodeBlockTests { // Arrange var functionRunner = new Mock(); - var context = new SKContext(this._functionRunner.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); var target = new CodeBlock("functionName", this._logger); this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -49,7 +51,7 @@ public class CodeBlockTests public async Task ItThrowsIfAFunctionCallThrowsAsync() { // Arrange - var context = new SKContext(this._functionRunner.Object, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, functions: this._functions.Object); var function = new Mock(); function .Setup(x => x.InvokeAsync(It.IsAny(), It.IsAny(), It.IsAny())) @@ -145,7 +147,7 @@ public class CodeBlockTests { // Arrange var variables = new ContextVariables { ["varName"] = "foo" }; - var context = new SKContext(this._functionRunner.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); // Act var codeBlock = new CodeBlock("$varName", NullLoggerFactory.Instance); @@ -160,7 +162,7 @@ public class CodeBlockTests { // Arrange var variables = new ContextVariables { ["varName"] = "bar" }; - var context = new SKContext(this._functionRunner.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); var varBlock = new VarBlock("$varName"); // Act @@ -175,7 +177,7 @@ public class CodeBlockTests public async Task ItRendersCodeBlockConsistingOfJustAValBlock1Async() { // Arrange - var context = new SKContext(this._functionRunner.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); // Act var codeBlock = new CodeBlock("'ciao'", NullLoggerFactory.Instance); @@ -190,7 +192,7 @@ public class CodeBlockTests { // Arrange var kernel = new Mock(); - var context = new SKContext(this._functionRunner.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); var valBlock = new ValBlock("'arrivederci'"); // Act @@ -209,7 +211,7 @@ public class CodeBlockTests const string Plugin = "pluginName"; var variables = new ContextVariables { ["input"] = "zero", ["var1"] = "uno", ["var2"] = "due" }; - var context = new SKContext(this._functionRunner.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var canary0 = string.Empty; @@ -257,7 +259,7 @@ public class CodeBlockTests const string VarValue = "varValue"; var variables = new ContextVariables { [Var] = VarValue }; - var context = new SKContext(this._functionRunner.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var varBlock = new VarBlock($"${Var}"); @@ -290,7 +292,7 @@ public class CodeBlockTests const string Plugin = "pluginName"; const string Value = "value"; - var context = new SKContext(this._functionRunner.Object, variables: null, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables: null, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var valBlock = new ValBlock($"'{Value}'"); @@ -328,7 +330,7 @@ public class CodeBlockTests var variables = new ContextVariables(); variables.Set("bob", BobValue); variables.Set("input", Value); - var context = new SKContext(this._functionRunner.Object, variables: variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables: variables, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var namedArgBlock1 = new NamedArgBlock($"foo='{FooValue}'"); var namedArgBlock2 = new NamedArgBlock("baz=$bob"); @@ -362,7 +364,7 @@ public class CodeBlockTests this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((pluginName, functionName, variables, cancellationToken) => { - var context = new SKContext(this._functionRunner.Object, variables); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables); return function.InvokeAsync(context, null, cancellationToken); }); } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs index c222beacd8..83a6be8cd7 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TemplateEngine.Basic; using Microsoft.SemanticKernel.TemplateEngine.Basic.Blocks; using Moq; @@ -29,6 +30,7 @@ public sealed class PromptTemplateEngineTests private readonly ITestOutputHelper _logger; private readonly Mock _kernel; private readonly Mock _functionRunner; + private readonly Mock _serviceProvider; public PromptTemplateEngineTests(ITestOutputHelper testOutputHelper) { @@ -38,6 +40,7 @@ public sealed class PromptTemplateEngineTests this._functions = new Mock(); this._kernel = new Mock(); this._functionRunner = new Mock(); + this._serviceProvider = new Mock(); } [Fact] @@ -375,7 +378,7 @@ public sealed class PromptTemplateEngineTests this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((pluginName, functionName, variables, cancellationToken) => { - var context = new SKContext(this._functionRunner.Object, variables); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables); return function.InvokeAsync(context, null, cancellationToken); }); } @@ -385,7 +388,7 @@ public sealed class PromptTemplateEngineTests this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((pluginName, functionName, variables, cancellationToken) => { - var context = new SKContext(this._functionRunner.Object, variables); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables); var function = functions.First(f => f.PluginName == functionName); return function.InvokeAsync(context, null, cancellationToken); @@ -396,6 +399,7 @@ public sealed class PromptTemplateEngineTests { return new SKContext( this._functionRunner.Object, + this._serviceProvider.Object, this._variables, this._functions.Object); } diff --git a/dotnet/src/IntegrationTests/Plugins/SamplePluginsTests.cs b/dotnet/src/IntegrationTests/Plugins/SamplePluginsTests.cs index d4817e2d61..040290ac3e 100644 --- a/dotnet/src/IntegrationTests/Plugins/SamplePluginsTests.cs +++ b/dotnet/src/IntegrationTests/Plugins/SamplePluginsTests.cs @@ -25,8 +25,6 @@ public class SamplePluginsTests { var function = kernel.Functions.GetFunction(view.PluginName, view.Name); Assert.NotNull(function); - Assert.NotNull(function.RequestSettings); - Assert.True(function.RequestSettings.ExtensionData.ContainsKey("max_tokens")); }); } @@ -49,8 +47,6 @@ public class SamplePluginsTests { var function = kernel.Functions.GetFunction(view.PluginName, view.Name); Assert.NotNull(function); - Assert.NotNull(function.RequestSettings); - Assert.True(function.RequestSettings.ExtensionData.ContainsKey("max_tokens")); }); } } diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs index 3c42aab053..8aff332f21 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -163,11 +164,12 @@ This plan uses the `GitHubPlugin.PullsList` function to list the open pull reque functions.Setup(x => x.GetFunctionViews()).Returns(new List()); } var functionRunner = new Mock(); + var serviceProvider = new Mock(); var kernel = new Mock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(testPlanString), functions.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(testPlanString), functions.Object); - var context = new SKContext(functionRunner.Object, functions: functions.Object); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, functions: functions.Object); var mockFunctionFlowFunction = new Mock(); mockFunctionFlowFunction.Setup(x => x.InvokeAsync( diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs index c4079d1b21..edcf0554af 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs @@ -2,6 +2,7 @@ using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -57,9 +58,10 @@ public class ReadOnlyFunctionCollectionExtensionsTests .Returns(asyncEnumerable); var functionRunner = new Mock(); + var serviceProvider = new Mock(); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, variables); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var config = InitializeConfig(t); var semanticQuery = "test"; @@ -136,9 +138,10 @@ public class ReadOnlyFunctionCollectionExtensionsTests functions.Setup(x => x.GetFunctionViews()).Returns(functionsView); var functionRunner = new Mock(); + var serviceProvider = new Mock(); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, variables, functions.Object); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables, functions.Object); var config = InitializeConfig(t); var semanticQuery = "test"; @@ -206,9 +209,10 @@ public class ReadOnlyFunctionCollectionExtensionsTests functions.Setup(x => x.GetFunctionViews()).Returns(functionsView); var functionRunner = new Mock(); + var serviceProvider = new Mock(); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, variables, functions.Object); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables, functions.Object); var config = InitializeConfig(t); config.SemanticMemoryConfig = new() { RelevancyThreshold = 0.78, Memory = memory.Object }; var semanticQuery = "test"; @@ -243,6 +247,7 @@ public class ReadOnlyFunctionCollectionExtensionsTests // Arrange var kernel = new Mock(); var functionRunner = new Mock(); + var serviceProvider = new Mock(); var variables = new ContextVariables(); var functions = new FunctionCollection(); @@ -267,7 +272,7 @@ public class ReadOnlyFunctionCollectionExtensionsTests .Returns(asyncEnumerable); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, variables); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var config = InitializeConfig(t); config.SemanticMemoryConfig = new() { RelevancyThreshold = 0.78, Memory = memory.Object }; var semanticQuery = "test"; diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs index 141ac5f572..263a75a8fc 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs @@ -4,6 +4,7 @@ using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TemplateEngine; using Moq; using Xunit; @@ -38,9 +39,10 @@ public class SequentialPlanParserTests private SKContext CreateSKContext( IFunctionRunner functionRunner, + IAIServiceProvider serviceProvider, ContextVariables? variables = null) { - return new SKContext(functionRunner, variables); + return new SKContext(functionRunner, serviceProvider, variables); } private static Mock CreateMockFunction(FunctionView functionView, string result = "") @@ -59,12 +61,13 @@ public class SequentialPlanParserTests kernel = kernelMock.Object; var functionRunnerMock = new Mock(); + var serviceProviderMock = new Mock(); // For Create kernelMock.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, skills, loggerFactory, culture) => { - return this.CreateSKContext(functionRunnerMock.Object, contextVariables); + return this.CreateSKContext(functionRunnerMock.Object, serviceProviderMock.Object, contextVariables); }); var functionsView = new List(); @@ -77,7 +80,7 @@ public class SequentialPlanParserTests var mockFunction = CreateMockFunction(functionView); functionsView.Add(functionView); - var result = this.CreateSKContext(functionRunnerMock.Object); + var result = this.CreateSKContext(functionRunnerMock.Object, serviceProviderMock.Object); result.Variables.Update(resultString); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) .ReturnsAsync(new FunctionResult(name, pluginName, result)); @@ -88,7 +91,8 @@ public class SequentialPlanParserTests It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny() + It.IsAny(), + It.IsAny() )).Returns(mockFunction.Object); } else diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs index 785eed2201..200f4088d9 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -60,6 +61,7 @@ public sealed class SequentialPlannerTests functions.Setup(x => x.GetFunctionViews()).Returns(functionsView); var functionRunner = new Mock(); + var serviceProvider = new Mock(); kernel.Setup(x => x.LoggerFactory).Returns(new Mock().Object); var expectedFunctions = input.Select(x => x.name).ToList(); @@ -67,10 +69,12 @@ public sealed class SequentialPlannerTests var context = new SKContext( functionRunner.Object, + serviceProvider.Object, new ContextVariables()); var returnContext = new SKContext( functionRunner.Object, + serviceProvider.Object, new ContextVariables()); var planString = @@ -147,15 +151,16 @@ public sealed class SequentialPlannerTests { // Arrange var functionRunner = new Mock(); + var serviceProvider = new Mock(); var kernel = new Mock(); var functions = new Mock(); functions.Setup(x => x.GetFunctionViews()).Returns(new List()); var planString = "notvalid<"; - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(planString)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(planString)); - var context = new SKContext(functionRunner.Object, new ContextVariables()); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables()); var mockFunctionFlowFunction = new Mock(); mockFunctionFlowFunction.Setup(x => x.InvokeAsync( diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs new file mode 100644 index 0000000000..3325f21ee0 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Services; + +#pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace - Using the main namespace +namespace Microsoft.SemanticKernel; +#pragma warning restore IDE0130 + +/// +/// Selector which will return a tuple containing instances of and from the specified provider based on the model settings. +/// +public interface IAIServiceSelector +{ + /// + /// Return the AI service and requesting settings from the specified provider based on the model settings. + /// The returned value is a tuple containing instances of and + /// + /// Type of AI service to return + /// AI service provider + /// Collection of model settings + /// + (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService; +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/ISKFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/ISKFunction.cs index 717639dab7..02565b7672 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/ISKFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/ISKFunction.cs @@ -33,11 +33,6 @@ public interface ISKFunction /// string Description { get; } - /// - /// AI service settings - /// - AIRequestSettings? RequestSettings { get; } - /// /// Returns a description of the function, including parameters. /// @@ -56,12 +51,21 @@ public interface ISKFunction AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default); + #region Obsolete + + /// + /// AI service settings + /// + [Obsolete("Use PromptTemplateConfig.ModelSettings instead. This will be removed in a future release.")] + AIRequestSettings? RequestSettings { get; } + /// /// Set the AI service used by the semantic function, passing a factory method. /// The factory allows to lazily instantiate the client and to properly handle its disposal. /// /// AI service factory /// Self instance + [Obsolete("Use implementation of IAIServiceConfigurationProvider instead. This will be removed in a future release.")] ISKFunction SetAIService(Func serviceFactory); /// @@ -69,10 +73,9 @@ public interface ISKFunction /// /// LLM completion settings /// Self instance + [Obsolete("Use implementation of IAIServiceConfigurationProvider instead. This will be removed in a future release.")] ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings); - #region Obsolete - /// /// Name of the plugin containing the function. The name is used by the function collection and in prompt templates e.g. {{skillName.functionName}} /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs b/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs index cb220a3ee6..ac239f2381 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs @@ -7,6 +7,7 @@ using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Orchestration; @@ -58,16 +59,23 @@ public sealed class SKContext /// public IFunctionRunner Runner { get; } + /// + /// AI service provider + /// + public IAIServiceProvider ServiceProvider { get; } + /// /// Constructor for the context. /// /// Function runner reference + /// AI service provider /// Context variables to include in context. /// Functions to include in context. /// Logger factory to be used in context /// Culture related to the context internal SKContext( IFunctionRunner functionRunner, + IAIServiceProvider serviceProvider, ContextVariables? variables = null, IReadOnlyFunctionCollection? functions = null, ILoggerFactory? loggerFactory = null, @@ -76,6 +84,7 @@ public sealed class SKContext Verify.NotNull(functionRunner, nameof(functionRunner)); this.Runner = functionRunner; + this.ServiceProvider = serviceProvider; this.Variables = variables ?? new(); this.Functions = functions ?? NullReadOnlyFunctionCollection.Instance; this.LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; @@ -110,6 +119,7 @@ public sealed class SKContext { return new SKContext( this.Runner, + this.ServiceProvider, variables ?? this.Variables.Clone(), functions ?? this.Functions, this.LoggerFactory, diff --git a/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs b/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs index ef8e7e06e5..d34f80b687 100644 --- a/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs @@ -7,7 +7,6 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AI; -using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.TemplateEngine; @@ -30,12 +29,14 @@ public static class KernelSemanticFunctionExtensions /// Name of the semantic function. The name can contain only alphanumeric chars + underscore. /// Prompt template configuration. /// Prompt template. + /// Optional, AI service selector. /// A C# function wrapping AI logic, usually defined with natural language public static ISKFunction RegisterSemanticFunction( this IKernel kernel, string functionName, PromptTemplateConfig promptTemplateConfig, - IPromptTemplate promptTemplate) + IPromptTemplate promptTemplate, + IAIServiceSelector? serviceSelector = null) { return kernel.RegisterSemanticFunction(FunctionCollection.GlobalFunctionsPluginName, functionName, promptTemplateConfig, promptTemplate); } @@ -48,13 +49,15 @@ public static class KernelSemanticFunctionExtensions /// Name of the semantic function. The name can contain only alphanumeric chars + underscore. /// Prompt template configuration. /// Prompt template. + /// Optional, AI service selector. /// A C# function wrapping AI logic, usually defined with natural language public static ISKFunction RegisterSemanticFunction( this IKernel kernel, string pluginName, string functionName, PromptTemplateConfig promptTemplateConfig, - IPromptTemplate promptTemplate) + IPromptTemplate promptTemplate, + IAIServiceSelector? serviceSelector = null) { // Future-proofing the name not to contain special chars Verify.ValidFunctionName(functionName); @@ -276,21 +279,16 @@ public static class KernelSemanticFunctionExtensions string pluginName, string functionName, PromptTemplateConfig promptTemplateConfig, - IPromptTemplate promptTemplate) + IPromptTemplate promptTemplate, + IAIServiceSelector? serviceSelector = null) { - ISKFunction func = SemanticFunction.FromSemanticConfig( + return SemanticFunction.FromSemanticConfig( pluginName, functionName, promptTemplateConfig, promptTemplate, + serviceSelector, kernel.LoggerFactory ); - - func.SetAIConfiguration(promptTemplateConfig.GetDefaultRequestSettings()); - - // Note: the service is instantiated using the kernel configuration state when the function is invoked - func.SetAIService(() => kernel.GetService(promptTemplateConfig.GetDefaultRequestSettings()?.ServiceId ?? null)); - - return func; } } diff --git a/dotnet/src/SemanticKernel.Core/Extensions/SKFunctionExtensions.cs b/dotnet/src/SemanticKernel.Core/Extensions/SKFunctionExtensions.cs index b855aa9b5d..533987024a 100644 --- a/dotnet/src/SemanticKernel.Core/Extensions/SKFunctionExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/Extensions/SKFunctionExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Globalization; using System.Threading; using System.Threading.Tasks; @@ -22,6 +23,7 @@ public static class SKFunctionExtensions /// Semantic function /// Request settings /// Self instance + [Obsolete("Use implementation of IAIServiceConfigurationProvider instead. This will be removed in a future release.")] public static ISKFunction UseCompletionSettings(this ISKFunction skFunction, AIRequestSettings requestSettings) { return skFunction.SetAIConfiguration(requestSettings); @@ -49,7 +51,7 @@ public static class SKFunctionExtensions CancellationToken cancellationToken = default) { var context = kernel.CreateNewContext(variables, functions, loggerFactory, culture); - return function.InvokeAsync(context, requestSettings ?? function.RequestSettings, cancellationToken); + return function.InvokeAsync(context, requestSettings, cancellationToken); } /// diff --git a/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs b/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs new file mode 100644 index 0000000000..47d310f3c3 --- /dev/null +++ b/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.AI.TextCompletion; +using Microsoft.SemanticKernel.Services; + +namespace Microsoft.SemanticKernel.Functions; + +/// +/// Added for backward compatibility only, this will be removed when ISKFunction.SetAIService and ISKFunction.SetAIConfiguration are removed. +/// +[Obsolete("Remove this when ISKFunction.SetAIService and ISKFunction.SetAIConfiguration are removed.")] +internal class DelegatingAIServiceSelector : IAIServiceSelector +{ + internal Func? ServiceFactory { get; set; } + internal AIRequestSettings? RequestSettings { get; set; } + + /// + public (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + { + return ((T?)this.ServiceFactory?.Invoke() ?? serviceProvider.GetService(null), this.RequestSettings ?? modelSettings?[0]); + } +} diff --git a/dotnet/src/SemanticKernel.Core/Functions/InstrumentedSKFunction.cs b/dotnet/src/SemanticKernel.Core/Functions/InstrumentedSKFunction.cs index 302f98aa18..cef67d4718 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/InstrumentedSKFunction.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/InstrumentedSKFunction.cs @@ -31,9 +31,6 @@ internal sealed class InstrumentedSKFunction : ISKFunction /// public string Description => this._function.Description; - /// - public AIRequestSettings? RequestSettings => this._function.RequestSettings; - /// /// Initialize a new instance of the class. /// @@ -78,14 +75,6 @@ internal sealed class InstrumentedSKFunction : ISKFunction this._function.InvokeAsync(context, requestSettings, cancellationToken)).ConfigureAwait(false); } - /// - public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) => - this._function.SetAIConfiguration(requestSettings); - - /// - public ISKFunction SetAIService(Func serviceFactory) => - this._function.SetAIService(serviceFactory); - #region private ================================================================================ private readonly ISKFunction _function; @@ -174,6 +163,20 @@ internal sealed class InstrumentedSKFunction : ISKFunction #region Obsolete ======================================================================= + /// + [Obsolete("Use ISKFunction.RequestSettingsFactory instead. This will be removed in a future release.")] + public AIRequestSettings? RequestSettings => this._function.RequestSettings; + + /// + [Obsolete("Use ISKFunction.SetAIRequestSettingsFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) => + this._function.SetAIConfiguration(requestSettings); + + /// + [Obsolete("Use ISKFunction.SetAIServiceFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIService(Func serviceFactory) => + this._function.SetAIService(serviceFactory); + /// [Obsolete("Methods, properties and classes which include Skill in the name have been renamed. Use ISKFunction.PluginName instead. This will be removed in a future release.")] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/dotnet/src/SemanticKernel.Core/Functions/NativeFunction.cs b/dotnet/src/SemanticKernel.Core/Functions/NativeFunction.cs index aa52bd7f81..da7ff333cd 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/NativeFunction.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/NativeFunction.cs @@ -44,9 +44,6 @@ internal sealed class NativeFunction : ISKFunction, IDisposable /// public string Description { get; } - /// - public AIRequestSettings? RequestSettings { get; } - /// /// List of function parameters /// @@ -150,20 +147,6 @@ internal sealed class NativeFunction : ISKFunction, IDisposable } } - /// - public ISKFunction SetAIService(Func serviceFactory) - { - this.ThrowNotSemantic(); - return this; - } - - /// - public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) - { - this.ThrowNotSemantic(); - return this; - } - /// /// Dispose of resources. /// @@ -873,6 +856,26 @@ internal sealed class NativeFunction : ISKFunction, IDisposable #region Obsolete + /// + [Obsolete("Use ISKFunction.RequestSettingsFactory instead. This will be removed in a future release.")] + public AIRequestSettings? RequestSettings { get; } + + /// + [Obsolete("Use ISKFunction.SetAIServiceFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIService(Func serviceFactory) + { + this.ThrowNotSemantic(); + return this; + } + + /// + [Obsolete("Use ISKFunction.SetAIRequestSettingsFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) + { + this.ThrowNotSemantic(); + return this; + } + /// [Obsolete("Methods, properties and classes which include Skill in the name have been renamed. Use ISKFunction.PluginName instead. This will be removed in a future release.")] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs b/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs new file mode 100644 index 0000000000..f1f8bf6432 --- /dev/null +++ b/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; + +namespace Microsoft.SemanticKernel.Functions; + +/// +/// Implementation of that selects the AI service based on the order of the model settings. +/// Uses the service id to select the preferred service provider and then returns the service and associated model settings. +/// +internal class OrderedIAIServiceSelector : IAIServiceSelector +{ + /// + public (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + { + if (modelSettings is null || modelSettings.Count == 0) + { + var service = serviceProvider.GetService(null); + if (service is not null) + { + return (service, null); + } + } + else + { + AIRequestSettings? defaultRequestSettings = null; + foreach (var model in modelSettings) + { + if (!string.IsNullOrEmpty(model.ServiceId)) + { + var service = serviceProvider.GetService(model.ServiceId); + if (service is not null) + { + return (service, model); + } + } + else + { + // First request settings with empty or null service id is the default + defaultRequestSettings ??= model; + } + } + + if (defaultRequestSettings is not null) + { + var service = serviceProvider.GetService(null); + if (service is not null) + { + return (service, defaultRequestSettings); + } + } + } + + var names = string.Join("|", modelSettings.Select(model => model.ServiceId).ToArray()); + throw new SKException($"Service of type {typeof(T)} and name {names ?? ""} not registered."); + } +} diff --git a/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs b/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs index bb5add248e..43e2923bc0 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.TemplateEngine; @@ -38,9 +39,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable /// public string Description { get; } - /// - public AIRequestSettings? RequestSettings { get; private set; } - /// /// List of function parameters /// @@ -53,6 +51,7 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable /// Name of the function to create. /// Prompt template configuration. /// Prompt template. + /// AI service selector /// The to use for logging. If null, no logging will be performed. /// The to monitor for cancellation requests. The default is . /// SK function instance. @@ -61,6 +60,7 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable string functionName, PromptTemplateConfig promptTemplateConfig, IPromptTemplate promptTemplate, + IAIServiceSelector? serviceSelector = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default) { @@ -72,9 +72,12 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable description: promptTemplateConfig.Description, pluginName: pluginName, functionName: functionName, + serviceSelector: serviceSelector, loggerFactory: loggerFactory - ); - func.SetAIConfiguration(promptTemplateConfig.GetDefaultRequestSettings()); + ) + { + _modelSettings = promptTemplateConfig.ModelSettings + }; return func; } @@ -93,22 +96,8 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable { this.AddDefaultValues(context.Variables); - return await this.RunPromptAsync(this._aiService?.Value, requestSettings ?? this.RequestSettings, context, cancellationToken).ConfigureAwait(false); - } - - /// - public ISKFunction SetAIService(Func serviceFactory) - { - Verify.NotNull(serviceFactory); - this._aiService = new Lazy(serviceFactory); - return this; - } - - /// - public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) - { - this.RequestSettings = requestSettings; - return this; + (var textCompletion, var defaultRequestSettings) = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); + return await this.RunPromptAsync(textCompletion, requestSettings ?? defaultRequestSettings, context, cancellationToken).ConfigureAwait(false); } /// @@ -116,10 +105,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable /// public void Dispose() { - if (this._aiService is { IsValueCreated: true } aiService) - { - (aiService.Value as IDisposable)?.Dispose(); - } } /// @@ -139,6 +124,7 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable string pluginName, string functionName, string description, + IAIServiceSelector? serviceSelector = null, ILoggerFactory? loggerFactory = null) { Verify.NotNull(template); @@ -154,6 +140,8 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable this.PluginName = pluginName; this.Description = description; + this._serviceSelector = serviceSelector ?? new OrderedIAIServiceSelector(); + this._view = new(() => new(functionName, pluginName, description, this.Parameters)); } @@ -162,7 +150,8 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable private static readonly JsonSerializerOptions s_toStringStandardSerialization = new(); private static readonly JsonSerializerOptions s_toStringIndentedSerialization = new() { WriteIndented = true }; private readonly ILogger _logger; - private Lazy? _aiService; + private IAIServiceSelector _serviceSelector; + public List? _modelSettings; private readonly Lazy _view; public IPromptTemplate _promptTemplate { get; } @@ -225,6 +214,46 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable #region Obsolete + /// + [Obsolete("Use ISKFunction.ModelSettings instead. This will be removed in a future release.")] + public AIRequestSettings? RequestSettings => this._modelSettings?.FirstOrDefault(); + + /// + [Obsolete("Use ISKFunction.SetAIServiceFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIService(Func serviceFactory) + { + Verify.NotNull(serviceFactory); + + if (this._serviceSelector is DelegatingAIServiceSelector delegatingProvider) + { + delegatingProvider.ServiceFactory = serviceFactory; + } + else + { + var serviceSelector = new DelegatingAIServiceSelector(); + serviceSelector.ServiceFactory = serviceFactory; + this._serviceSelector = serviceSelector; + } + return this; + } + + /// + [Obsolete("Use ISKFunction.SetAIRequestSettingsFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) + { + if (this._serviceSelector is DelegatingAIServiceSelector delegatingProvider) + { + delegatingProvider.RequestSettings = requestSettings; + } + else + { + var configurationProvider = new DelegatingAIServiceSelector(); + configurationProvider.RequestSettings = requestSettings; + this._serviceSelector = configurationProvider; + } + return this; + } + /// [Obsolete("Methods, properties and classes which include Skill in the name have been renamed. Use ISKFunction.PluginName instead. This will be removed in a future release.")] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/dotnet/src/SemanticKernel.Core/Kernel.cs b/dotnet/src/SemanticKernel.Core/Kernel.cs index 92c582e975..d7be7c73a7 100644 --- a/dotnet/src/SemanticKernel.Core/Kernel.cs +++ b/dotnet/src/SemanticKernel.Core/Kernel.cs @@ -174,6 +174,7 @@ repeat: { return new SKContext( new FunctionRunner(this), + this._aiServiceProvider, variables, functions ?? this.Functions, loggerFactory ?? this.LoggerFactory, diff --git a/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs b/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs index b6808c6130..61e39ca9ee 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs @@ -28,9 +28,6 @@ internal sealed class InstrumentedPlan : IPlan /// public string Description => this._plan.Description; - /// - public AIRequestSettings? RequestSettings => this._plan.RequestSettings; - /// /// Initialize a new instance of the class. /// @@ -60,14 +57,6 @@ internal sealed class InstrumentedPlan : IPlan this._plan.InvokeAsync(context, requestSettings, cancellationToken)).ConfigureAwait(false); } - /// - public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) => - this._plan.SetAIConfiguration(requestSettings); - - /// - public ISKFunction SetAIService(Func serviceFactory) => - this._plan.SetAIService(serviceFactory); - #region private ================================================================================ private readonly IPlan _plan; @@ -155,6 +144,20 @@ internal sealed class InstrumentedPlan : IPlan #region Obsolete ======================================================================= + /// + [Obsolete("Use ISKFunction.RequestSettingsFactory instead. This will be removed in a future release.")] + public AIRequestSettings? RequestSettings => this._plan.RequestSettings; + + /// + [Obsolete("Use ISKFunction.SetAIRequestSettingsFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) => + this._plan.SetAIConfiguration(requestSettings); + + /// + [Obsolete("Use ISKFunction.SetAIServiceFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIService(Func serviceFactory) => + this._plan.SetAIService(serviceFactory); + /// [Obsolete("Methods, properties and classes which include Skill in the name have been renamed. Use ISKFunction.PluginName instead. This will be removed in a future release.")] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs index d68b47a783..46c954ad07 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs @@ -77,8 +77,8 @@ public sealed class Plan : IPlan public string Description { get; set; } = string.Empty; /// - [JsonIgnore] - public AIRequestSettings? RequestSettings { get; private set; } + [JsonPropertyName("model_settings")] + public List? ModelSettings { get; private set; } #endregion ISKFunction implementation @@ -359,18 +359,6 @@ public sealed class Plan : IPlan return result; } - /// - public ISKFunction SetAIService(Func serviceFactory) - { - return this.Function is not null ? this.Function.SetAIService(serviceFactory) : this; - } - - /// - public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) - { - return this.Function is not null ? this.Function.SetAIConfiguration(requestSettings) : this; - } - #endregion ISKFunction implementation /// @@ -597,9 +585,9 @@ public sealed class Plan : IPlan this.Name = function.Name; this.PluginName = function.PluginName; this.Description = function.Description; - this.RequestSettings = function.RequestSettings; #pragma warning disable CS0618 // Type or member is obsolete + this.RequestSettings = function.RequestSettings; this.IsSemantic = function.IsSemantic; #pragma warning restore CS0618 // Type or member is obsolete } @@ -637,6 +625,24 @@ public sealed class Plan : IPlan #region Obsolete + /// + [Obsolete("Use ISKFunction.ModelSettings instead. This will be removed in a future release.")] + public AIRequestSettings? RequestSettings { get; private set; } + + /// + [Obsolete("Use ISKFunction.SetAIServiceFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIService(Func serviceFactory) + { + return this.Function is not null ? this.Function.SetAIService(serviceFactory) : this; + } + + /// + [Obsolete("Use ISKFunction.SetAIRequestSettingsFactory instead. This will be removed in a future release.")] + public ISKFunction SetAIConfiguration(AIRequestSettings? requestSettings) + { + return this.Function is not null ? this.Function.SetAIConfiguration(requestSettings) : this; + } + /// [JsonIgnore] [Obsolete("Methods, properties and classes which include Skill in the name have been renamed. Use ISKFunction.PluginName instead. This will be removed in a future release.")] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCollectionTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCollectionTests.cs index addbedc144..5023318384 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCollectionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCollectionTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AI; using Moq; @@ -13,17 +14,18 @@ public class FunctionCollectionTests public void ItAllowsToReplaceFunctions() { // Arrange + List modelSettings = new(); + modelSettings.Add(new AIRequestSettings()); + var functionOne = new Mock(); functionOne.SetupGet(x => x.Name).Returns("fName"); functionOne.SetupGet(x => x.PluginName).Returns("sName"); functionOne.SetupGet(x => x.Description).Returns("ONE"); - functionOne.SetupGet(x => x.RequestSettings).Returns(new AIRequestSettings()); var functionTwo = new Mock(); functionTwo.SetupGet(x => x.Name).Returns("fName"); functionTwo.SetupGet(x => x.PluginName).Returns("sName"); functionTwo.SetupGet(x => x.Description).Returns("TWO"); - functionTwo.SetupGet(x => x.RequestSettings).Returns(new AIRequestSettings()); var target = new FunctionCollection(); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs new file mode 100644 index 0000000000..23fb743e9c --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.AI.TextCompletion; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.TemplateEngine; +using Moq; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; +public class MultipleModelTests +{ + [Fact] + public async Task ItUsesServiceIdWhenProvidedAsync() + { + // Arrange + var mockTextCompletion1 = new Mock(); + var mockTextCompletion2 = new Mock(); + var mockCompletionResult = new Mock(); + + mockTextCompletion1.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockTextCompletion2.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); + + var kernel = Kernel.Builder + .WithAIService("service1", mockTextCompletion1.Object, false) + .WithAIService("service2", mockTextCompletion2.Object, true) + .Build(); + + var templateConfig = new PromptTemplateConfig(); + templateConfig.ModelSettings.Add(new AIRequestSettings() { ServiceId = "service1" }); + var func = kernel.CreateSemanticFunction("template", templateConfig, "functionName", "pluginName"); + + // Act + await kernel.RunAsync(func); + + // Assert + mockTextCompletion1.Verify(a => a.GetCompletionsAsync("template", It.IsAny(), It.IsAny()), Times.Once()); + mockTextCompletion2.Verify(a => a.GetCompletionsAsync("template", It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ItFailsIfInvalidServiceIdIsProvidedAsync() + { + // Arrange + var mockTextCompletion1 = new Mock(); + var mockTextCompletion2 = new Mock(); + + var kernel = Kernel.Builder + .WithAIService("service1", mockTextCompletion1.Object, false) + .WithAIService("service2", mockTextCompletion2.Object, true) + .Build(); + + var templateConfig = new PromptTemplateConfig(); + templateConfig.ModelSettings.Add(new AIRequestSettings() { ServiceId = "service3" }); + var func = kernel.CreateSemanticFunction("template", templateConfig, "functionName", "pluginName"); + + // Act + var exception = await Assert.ThrowsAsync(() => kernel.RunAsync(func)); + + // Assert + Assert.Equal("Service of type Microsoft.SemanticKernel.AI.TextCompletion.ITextCompletion and name service3 not registered.", exception.Message); + } + + [Theory] + [InlineData(new string[] { "service1" }, 1, new int[] { 1, 0, 0 })] + [InlineData(new string[] { "service2" }, 2, new int[] { 0, 1, 0 })] + [InlineData(new string[] { "service3" }, 0, new int[] { 0, 0, 1 })] + [InlineData(new string[] { "service4", "service1" }, 1, new int[] { 1, 0, 0 })] + public async Task ItUsesServiceIdByOrderAsync(string[] serviceIds, int defaultServiceIndex, int[] callCount) + { + // Arrange + var mockTextCompletion1 = new Mock(); + var mockTextCompletion2 = new Mock(); + var mockTextCompletion3 = new Mock(); + var mockCompletionResult = new Mock(); + + mockTextCompletion1.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockTextCompletion2.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockTextCompletion3.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); + + var kernel = Kernel.Builder + .WithAIService("service1", mockTextCompletion1.Object, defaultServiceIndex == 0) + .WithAIService("service2", mockTextCompletion2.Object, defaultServiceIndex == 1) + .WithAIService("service3", mockTextCompletion3.Object, defaultServiceIndex == 2) + .Build(); + + var templateConfig = new PromptTemplateConfig(); + foreach (var serviceId in serviceIds) + { + templateConfig.ModelSettings.Add(new AIRequestSettings() { ServiceId = serviceId }); + } + var func = kernel.CreateSemanticFunction("template", templateConfig, "functionName", "pluginName"); + + // Act + await kernel.RunAsync(func); + + // Assert + mockTextCompletion1.Verify(a => a.GetCompletionsAsync("template", It.Is(settings => settings.ServiceId == "service1"), It.IsAny()), Times.Exactly(callCount[0])); + mockTextCompletion2.Verify(a => a.GetCompletionsAsync("template", It.Is(settings => settings.ServiceId == "service2"), It.IsAny()), Times.Exactly(callCount[1])); + mockTextCompletion3.Verify(a => a.GetCompletionsAsync("template", It.Is(settings => settings.ServiceId == "service3"), It.IsAny()), Times.Exactly(callCount[2])); + } + + [Fact] + public async Task ItUsesServiceIdWithJsonPromptTemplateConfigAsync() + { + // Arrange + var mockTextCompletion1 = new Mock(); + var mockTextCompletion2 = new Mock(); + var mockTextCompletion3 = new Mock(); + var mockCompletionResult = new Mock(); + + mockTextCompletion1.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockTextCompletion2.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockTextCompletion3.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); + mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); + + var kernel = Kernel.Builder + .WithAIService("service1", mockTextCompletion1.Object, true) + .WithAIService("service2", mockTextCompletion2.Object, false) + .WithAIService("service3", mockTextCompletion3.Object, false) + .Build(); + + var json = @"{ + ""schema"": 1, + ""description"": ""Semantic function"", + ""models"": [ + { + ""service_id"": ""service2"", + ""max_tokens"": 100, + ""temperature"": 0.2, + ""top_p"": 0.0, + ""presence_penalty"": 0.0, + ""frequency_penalty"": 0.0, + ""stop_sequences"": [ + ""\n"" + ] + }, + { + ""service_id"": ""service3"", + ""max_tokens"": 100, + ""temperature"": 0.4, + ""top_p"": 0.0, + ""presence_penalty"": 0.0, + ""frequency_penalty"": 0.0, + ""stop_sequences"": [ + ""\n"" + ] + } + ] +}"; + + var templateConfig = PromptTemplateConfig.FromJson(json); + var func = kernel.CreateSemanticFunction("template", templateConfig, "functionName", "pluginName"); + + // Act + await kernel.RunAsync(func); + + // Assert + mockTextCompletion1.Verify(a => a.GetCompletionsAsync("template", It.Is(settings => settings.ServiceId == "service1"), It.IsAny()), Times.Never()); + mockTextCompletion2.Verify(a => a.GetCompletionsAsync("template", It.Is(settings => settings.ServiceId == "service2"), It.IsAny()), Times.Once()); + mockTextCompletion3.Verify(a => a.GetCompletionsAsync("template", It.Is(settings => settings.ServiceId == "service3"), It.IsAny()), Times.Never()); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs new file mode 100644 index 0000000000..5159695c30 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.AI.TextCompletion; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions; +using Microsoft.SemanticKernel.Services; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; +public class OrderedIAIServiceConfigurationProviderTests +{ + [Fact] + public void ItThrowsAnSKExceptionForNoServices() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List(); + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + // Assert + Assert.Throws(() => configurationProvider.SelectAIService(serviceProvider, modelSettings)); + } + + [Fact] + public void ItGetsAIServiceConfigurationForSingleAIService() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService(new AIService()); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List(); + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.NotNull(aiService); + Assert.Null(defaultRequestSettings); + } + + [Fact] + public void ItGetsAIServiceConfigurationForSingleTextCompletion() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService(new TextCompletion()); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List(); + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.NotNull(aiService); + Assert.Null(defaultRequestSettings); + } + + [Fact] + public void ItAIServiceConfigurationForTextCompletionByServiceId() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion()); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List(); + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.NotNull(aiService); + Assert.Null(defaultRequestSettings); + } + + [Fact] + public void ItThrowsAnSKExceptionForNotFoundService() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion()); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List + { + new AIRequestSettings() { ServiceId = "service3" } + }; + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + // Assert + Assert.Throws(() => configurationProvider.SelectAIService(serviceProvider, modelSettings)); + } + + [Fact] + public void ItUsesDefaultServiceForNullModelSettings() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion(), true); + var serviceProvider = serviceCollection.Build(); + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, null); + + // Assert + Assert.Equal(serviceProvider.GetService("service2"), aiService); + Assert.Null(defaultRequestSettings); + } + + [Fact] + public void ItUsesDefaultServiceForEmptyModelSettings() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion(), true); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List(); + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.Equal(serviceProvider.GetService("service2"), aiService); + Assert.Null(defaultRequestSettings); + } + + [Fact] + public void ItUsesDefaultServiceAndSettings() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion(), true); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List + { + new AIRequestSettings() + }; + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.Equal(serviceProvider.GetService("service2"), aiService); + Assert.Equal(modelSettings[0], defaultRequestSettings); + } + + [Fact] + public void ItUsesDefaultServiceAndSettingsEmptyServiceId() + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion(), true); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List + { + new AIRequestSettings() { ServiceId = "" } + }; + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.Equal(serviceProvider.GetService("service2"), aiService); + Assert.Equal(modelSettings[0], defaultRequestSettings); + } + + [Theory] + [InlineData(new string[] { "service1" }, "service1")] + [InlineData(new string[] { "service2" }, "service2")] + [InlineData(new string[] { "service3" }, "service3")] + [InlineData(new string[] { "service4", "service1" }, "service1")] + public void ItGetsAIServiceConfigurationByOrder(string[] serviceIds, string expectedServiceId) + { + // Arrange + var serviceCollection = new AIServiceCollection(); + serviceCollection.SetService("service1", new TextCompletion()); + serviceCollection.SetService("service2", new TextCompletion()); + serviceCollection.SetService("service3", new TextCompletion()); + var serviceProvider = serviceCollection.Build(); + var modelSettings = new List(); + foreach (var serviceId in serviceIds) + { + modelSettings.Add(new AIRequestSettings() { ServiceId = serviceId }); + } + var configurationProvider = new OrderedIAIServiceSelector(); + + // Act + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + + // Assert + Assert.Equal(serviceProvider.GetService(expectedServiceId), aiService); + Assert.Equal(expectedServiceId, defaultRequestSettings!.ServiceId); + } + + #region private + private sealed class AIService : IAIService + { + } + + private sealed class TextCompletion : ITextCompletion + { + public Task> GetCompletionsAsync(string text, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable GetStreamingCompletionsAsync(string text, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } + #endregion +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs index 2686395430..dfb60d5716 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -22,7 +23,7 @@ public class SKContextTests { // Arrange var variables = new ContextVariables(); - var target = new SKContext(new Mock().Object, variables); + var target = new SKContext(new Mock().Object, new Mock().Object, variables); variables.Set("foo1", "bar1"); // Act @@ -46,8 +47,8 @@ public class SKContextTests // Arrange IDictionary functions = KernelBuilder.Create().ImportFunctions(new Parrot(), "test"); this._functions.Setup(x => x.GetFunction("func")).Returns(functions["say"]); - var (kernel, functionRunner) = this.SetupKernelMock(this._functions.Object); - var target = new SKContext(functionRunner.Object, new ContextVariables(), this._functions.Object); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(this._functions.Object); + var target = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(), this._functions.Object); Assert.NotNull(target.Functions); // Act @@ -60,21 +61,22 @@ public class SKContextTests Assert.Equal("ciao", result.GetValue()); } - private (Mock kernelMock, Mock functionRunnerMock) SetupKernelMock(IReadOnlyFunctionCollection? functions = null) + private (Mock kernelMock, Mock functionRunnerMock, Mock serviceProviderMock) SetupKernelMock(IReadOnlyFunctionCollection? functions = null) { functions ??= new Mock().Object; var kernel = new Mock(); var functionRunner = new Mock(); + var serviceProvider = new Mock(); kernel.SetupGet(x => x.Functions).Returns(functions); kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, skills, loggerFactory, culture) => { - return new SKContext(functionRunner.Object, contextVariables); + return new SKContext(functionRunner.Object, serviceProvider.Object, contextVariables); }); - return (kernel, functionRunner); + return (kernel, functionRunner, serviceProvider); } private sealed class Parrot diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs index 8a128a37a5..b717eb398c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -1044,9 +1045,11 @@ public sealed class SKFunctionTests2 private SKContext MockContext(string input) { var functionRunner = new Mock(); + var serviceProvider = new Mock(); return new SKContext( functionRunner.Object, + serviceProvider.Object, new ContextVariables(input) ); } diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs index b80a68a8f2..4de7067cf1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs @@ -169,11 +169,10 @@ public class SemanticFunctionTests public async Task RunAsyncHandlesPreInvocationAsync(int pipelineCount) { // Arrange - var sut = Kernel.Builder.Build(); - var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var (mockTextResult, mockTextCompletion) = this.SetupMocks(); + var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); - semanticFunction.SetAIService(() => mockTextCompletion.Object); var invoked = 0; sut.FunctionInvoking += (sender, e) => { @@ -219,10 +218,9 @@ public class SemanticFunctionTests public async Task RunAsyncHandlesPreInvocationCancelationDontRunSubsequentFunctionsInThePipelineAsync() { // Arrange - var sut = Kernel.Builder.Build(); var (mockTextResult, mockTextCompletion) = this.SetupMocks(); + var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); - semanticFunction.SetAIService(() => mockTextCompletion.Object); var invoked = 0; sut.FunctionInvoking += (sender, e) => @@ -268,11 +266,10 @@ public class SemanticFunctionTests public async Task RunAsyncPreInvocationSkipDontTriggerInvokedHandlerAsync() { // Arrange - var sut = Kernel.Builder.Build(); var (mockTextResult, mockTextCompletion) = this.SetupMocks(); + var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); var semanticFunction1 = sut.CreateSemanticFunction("Write one phrase about UnitTests", functionName: "SkipMe"); var semanticFunction2 = sut.CreateSemanticFunction("Write two phrases about UnitTests", functionName: "DontSkipMe"); - semanticFunction2.SetAIService(() => mockTextCompletion.Object); var invoked = 0; var invoking = 0; string invokedFunction = string.Empty; @@ -309,11 +306,10 @@ public class SemanticFunctionTests public async Task RunAsyncHandlesPostInvocationAsync(int pipelineCount) { // Arrange - var sut = Kernel.Builder.Build(); - var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var (mockTextResult, mockTextCompletion) = this.SetupMocks(); + var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); - semanticFunction.SetAIService(() => mockTextCompletion.Object); var invoked = 0; sut.FunctionInvoked += (sender, e) => @@ -338,11 +334,10 @@ public class SemanticFunctionTests [Fact] public async Task RunAsyncChangeVariableInvokingHandlerAsync() { - var sut = Kernel.Builder.Build(); + var (mockTextResult, mockTextCompletion) = this.SetupMocks(); + var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); var prompt = "Write a simple phrase about UnitTests {{$input}}"; var semanticFunction = sut.CreateSemanticFunction(prompt); - var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - semanticFunction.SetAIService(() => mockTextCompletion.Object); var originalInput = "Importance"; var newInput = "Problems"; @@ -362,11 +357,10 @@ public class SemanticFunctionTests [Fact] public async Task RunAsyncChangeVariableInvokedHandlerAsync() { - var sut = Kernel.Builder.Build(); + var (mockTextResult, mockTextCompletion) = this.SetupMocks(); + var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); var prompt = "Write a simple phrase about UnitTests {{$input}}"; var semanticFunction = sut.CreateSemanticFunction(prompt); - var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - semanticFunction.SetAIService(() => mockTextCompletion.Object); var originalInput = "Importance"; var newInput = "Problems"; @@ -396,15 +390,13 @@ public class SemanticFunctionTests const string PluginName = "MyPlugin"; const string Prompt = "Write a simple phrase about UnitTests"; - var kernel = Kernel.Builder.Build(); + var (mockTextResult, mockTextCompletion) = this.SetupMocks("Result3"); + var kernel = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); var function1 = SKFunction.FromNativeMethod(Method(Function1), pluginName: PluginName); var function2 = SKFunction.FromNativeMethod(Method(Function2), pluginName: PluginName); var function3 = kernel.CreateSemanticFunction(Prompt, functionName: "Function3", pluginName: PluginName); - var (mockTextResult, mockTextCompletion) = this.SetupMocks("Result3"); - - function3.SetAIService(() => mockTextCompletion.Object); // Act var kernelResult = await kernel.RunAsync(function1, function2, function3); diff --git a/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs b/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs index caf17df58a..62d455fd34 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs @@ -2,6 +2,7 @@ using System; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -13,10 +14,11 @@ namespace SemanticKernel.UnitTests.Orchestration; public class FunctionResultTests { private readonly Mock _functionRunner = new(); + private readonly Mock _serviceProvider = new(); private SKContext CreateContext() { - return new SKContext(this._functionRunner.Object); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs b/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs index f37023ab8f..17b68facf2 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -15,11 +16,12 @@ namespace SemanticKernel.UnitTests.Orchestration; public class KernelResultTests { private readonly Mock _functionRunner = new(); + private readonly Mock _serviceProvider = new(); private readonly SKContext _context; public KernelResultTests() { - this._context = new SKContext(this._functionRunner.Object); + this._context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs index bfea57b580..5791f6e236 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Planning; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -18,6 +19,7 @@ namespace SemanticKernel.UnitTests.Planning; public sealed class PlanSerializationTests { private readonly Mock _functionRunner = new(); + private readonly Mock _serviceProvider = new(); [Fact] public void CanSerializePlan() @@ -87,7 +89,7 @@ public sealed class PlanSerializationTests // Arrange Mocks var functions = new Mock(); - var returnContext = new SKContext(this._functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -120,7 +122,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); @@ -155,7 +157,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); @@ -190,7 +192,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); @@ -224,7 +226,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); @@ -262,10 +264,10 @@ public sealed class PlanSerializationTests kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, functions, loggerFactory, culture) => { - return new SKContext(this._functionRunner.Object, contextVariables, functions); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, contextVariables, functions); }); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput) + var returnContext = new SKContext(functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); var mockFunction = new Mock(); @@ -277,7 +279,7 @@ public sealed class PlanSerializationTests this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((function, variables, ct) => { - var c = new SKContext(new Mock().Object, variables); + var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input); var functionResult = new FunctionResult(function.Name, function.PluginName, returnContext); return Task.FromResult(functionResult); @@ -334,11 +336,11 @@ public sealed class PlanSerializationTests kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, functions, loggerFactory, culture) => { - return new SKContext(this._functionRunner.Object, contextVariables); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, contextVariables); }); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); @@ -356,7 +358,7 @@ public sealed class PlanSerializationTests this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((function, variables, ct) => { - var c = new SKContext(new Mock().Object, variables); + var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); c.Variables.TryGetValue("variables", out string? v); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v); @@ -423,14 +425,14 @@ public sealed class PlanSerializationTests kernel.SetupGet(x => x.Functions).Returns(functions.Object); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, functions, loggerFactory, culture) => { - return new SKContext(this._functionRunner.Object, contextVariables); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, contextVariables); }); var mockFunction = new Mock(); @@ -459,7 +461,7 @@ public sealed class PlanSerializationTests this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((function, variables, ct) => { - var c = new SKContext(new Mock().Object, variables); + var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); c.Variables.TryGetValue("variables", out string? v); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v); @@ -487,7 +489,7 @@ public sealed class PlanSerializationTests cv.Update(string.Empty); var nextContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables() ); plan = Plan.FromJson(serializedPlan1, functions.Object); @@ -522,7 +524,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); @@ -577,7 +579,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, + this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) ); diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs index 1b157593b8..28b78b558d 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs @@ -9,6 +9,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Planning; +using Microsoft.SemanticKernel.Services; using Moq; using Xunit; @@ -34,7 +35,7 @@ public sealed class PlanTests public async Task CanExecutePlanAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); var goal = "Write a poem or joke and send it in an e-mail to Kai."; var plan = new Plan(goal); @@ -53,9 +54,10 @@ public sealed class PlanTests // Arrange var goal = "Write a poem or joke and send it in an e-mail to Kai."; var plan = new Plan(goal); - var kernel = new Mock(); + var functionRunner = new Mock(); + var serviceProvider = new Mock(); - var context = new SKContext(kernel.Object, new ContextVariables("Some input")); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables("Some input")); // Act var result = await plan.InvokeAsync(context); @@ -85,9 +87,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -118,9 +120,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -151,9 +153,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -184,9 +186,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -217,9 +219,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -250,9 +252,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput) + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput) ); var mockFunction = new Mock(); @@ -290,9 +292,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -339,9 +341,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -367,9 +369,9 @@ public sealed class PlanTests // Arrange var logger = new Mock(); var functions = new Mock(); - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -393,9 +395,9 @@ public sealed class PlanTests var subPlan = new Plan("Write a poem or joke"); // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var childFunction1 = new Mock(); childFunction1.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -474,9 +476,9 @@ public sealed class PlanTests public async Task CanExecutePlanWithOneStepAndStateAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -502,17 +504,17 @@ public sealed class PlanTests public async Task CanExecutePlanWithStateAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(async (function, variables, ct) => { - var c = new SKContext(functionRunner.Object, variables); + var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); return functionResult; }); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -545,9 +547,9 @@ public sealed class PlanTests public async Task CanExecutePlanWithCustomContextAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -576,7 +578,7 @@ public sealed class PlanTests plan.State.Set("input", "Cleopatra"); plan.State.Set("type", "poem"); - var contextOverride = new SKContext(functionRunner.Object); + var contextOverride = new SKContext(functionRunner.Object, serviceProvider.Object); contextOverride.Variables.Set("type", "joke"); contextOverride.Variables.Update("Medusa"); @@ -594,9 +596,9 @@ public sealed class PlanTests public async Task CanExecutePlanWithCustomStateAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -646,7 +648,7 @@ public sealed class PlanTests planStep.Parameters.Set("input", "Cleopatra"); planStep.Parameters.Set("type", "poem"); plan.AddSteps(planStep); - var contextOverride = new SKContext(functionRunner.Object); + var contextOverride = new SKContext(functionRunner.Object, serviceProvider.Object); contextOverride.Variables.Set("type", "joke"); contextOverride.Variables.Update("Medusa"); // context input will not override parameters @@ -664,17 +666,17 @@ public sealed class PlanTests public async Task CanExecutePlanWithJoinedResultAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(async (function, variables, ct) => { - var c = new SKContext(functionRunner.Object, variables); + var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); return functionResult; }); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var outlineMock = new Mock(); outlineMock.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -794,9 +796,9 @@ Previously:Outline section #1 of 3: Here is a 3 chapter outline about NovelOutli public async Task CanExecutePlanWithExpandedAsync() { // Arrange - var (kernel, functionRunner) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); var functionMock = new Mock(); functionMock.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -825,28 +827,29 @@ Previously:Outline section #1 of 3: Here is a 3 chapter outline about NovelOutli Assert.Equal(expected, result.GetValue()); } - private (Mock kernelMock, Mock functionRunnerMock) SetupKernelMock(IFunctionCollection? functions = null) + private (Mock kernelMock, Mock functionRunnerMock, Mock serviceProviderMock) SetupKernelMock(IFunctionCollection? functions = null) { functions ??= new Mock().Object; var kernel = new Mock(); var functionRunner = new Mock(); + var serviceProvider = new Mock(); kernel.SetupGet(x => x.Functions).Returns(functions); kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, skills, loggerFactory, culture) => { - return new SKContext(functionRunner.Object, contextVariables, functions); + return new SKContext(functionRunner.Object, serviceProvider.Object, contextVariables, functions); }); functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(async (function, variables, ct) => { - var c = new SKContext(functionRunner.Object, variables); + var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); return functionResult; }); - return (kernel, functionRunner); + return (kernel, functionRunner, serviceProvider); } } From ba40209e2405079eb714b94e4c314a4614b8b741 Mon Sep 17 00:00:00 2001 From: feiyun0112 Date: Tue, 24 Oct 2023 19:01:28 +0800 Subject: [PATCH 05/24] .Net: Remove Kernel.Builder (#3180) #3169 Use `new KernelBuilder()` instead of `Kernel.Builder` --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../Example06_TemplateLanguage.cs | 2 +- .../Example08_RetryHandler.cs | 2 +- .../Example09_FunctionTypes.cs | 2 +- ...xample10_DescribeAllPluginsAndFunctions.cs | 2 +- .../Example11_WebSearchQueries.cs | 2 +- .../Example13_ConversationSummaryPlugin.cs | 2 +- .../Example15_TextMemoryPlugin.cs | 2 +- .../Example40_DIContainer.cs | 2 +- .../Example41_HttpClientUsage.cs | 8 ++-- .../Example42_KernelBuilder.cs | 22 +++++----- .../Example52_ApimAuth.cs | 2 +- ...ateNativeFunctionsWithMultipleArguments.cs | 2 +- .../Example58_ConfigureRequestSettings.cs | 2 +- .../Connectors.Memory.Chroma/README.md | 2 +- .../Connectors.Memory.Kusto/README.md | 2 +- .../Connectors.Memory.Milvus/README.md | 2 +- .../Connectors.Memory.Postgres/README.md | 2 +- .../Connectors.Memory.Redis/README.md | 2 +- .../OpenAI/AIServicesOpenAIExtensionsTests.cs | 6 +-- .../KernelSemanticFunctionExtensionsTests.cs | 4 +- .../IntegrationTests/Planners/PlanTests.cs | 2 +- .../SequentialPlanParserTests.cs | 2 +- .../SequentialPlannerTests.cs | 2 +- .../StepwisePlanner/StepwisePlannerTests.cs | 2 +- .../PromptTemplateEngineTests.cs | 16 +++---- .../WebPlugin/WebPluginTests.cs | 4 +- .../InternalUtilities/test/FunctionHelpers.cs | 2 +- .../Plugins.Core/ConversationSummaryPlugin.cs | 2 +- .../Core/FileIOPluginTests.cs | 2 +- .../Plugins.UnitTests/Core/MathPluginTests.cs | 2 +- .../Plugins.UnitTests/Core/TextPluginTests.cs | 2 +- .../Plugins.UnitTests/Core/TimePluginTests.cs | 2 +- .../Plugins.UnitTests/Core/WaitPluginTests.cs | 2 +- .../Web/SearchUrlSkillTests.cs | 2 +- dotnet/src/SemanticKernel.Core/Kernel.cs | 1 + .../Functions/MultipleModelTests.cs | 8 ++-- .../Functions/SKFunctionTests3.cs | 6 +-- .../Functions/SemanticFunctionTests.cs | 30 ++++++------- .../SemanticKernel.UnitTests/KernelTests.cs | 42 +++++++++---------- .../KernelHttpServer/SemanticKernelFactory.cs | 2 +- .../dotnet/MsGraphPluginsExample/Program.cs | 2 +- .../dotnet/OpenApiPluginsExample/Program.cs | 8 ++-- 42 files changed, 108 insertions(+), 107 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs b/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs index cd1816e54b..9181215243 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs @@ -28,7 +28,7 @@ public static class Example06_TemplateLanguage return; } - IKernel kernel = Kernel.Builder + IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAIChatCompletionService( modelId: openAIModelId, diff --git a/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs b/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs index 0adeec6e09..576b3dd13d 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs @@ -75,7 +75,7 @@ public static class Example08_RetryHandler private static KernelBuilder InitializeKernelBuilder() { - return Kernel.Builder + return new KernelBuilder() .WithLoggerFactory(InfoLogger.LoggerFactory) // OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play .WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, "BAD_KEY"); diff --git a/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs b/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs index 73905cac77..ff5c365641 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs @@ -15,7 +15,7 @@ public static class Example09_FunctionTypes { Console.WriteLine("======== Native function types ========"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .Build(); diff --git a/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs b/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs index d49bda2e30..1cd68f4c0f 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs @@ -20,7 +20,7 @@ public static class Example10_DescribeAllPluginsAndFunctions { Console.WriteLine("======== Describe all plugins and functions ========"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithOpenAIChatCompletionService( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey) diff --git a/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs b/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs index f6c06cef9d..888f78a3ed 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs @@ -13,7 +13,7 @@ public static class Example11_WebSearchQueries { Console.WriteLine("======== WebSearchQueries ========"); - IKernel kernel = Kernel.Builder.WithLoggerFactory(ConsoleLogger.LoggerFactory).Build(); + IKernel kernel = new KernelBuilder().WithLoggerFactory(ConsoleLogger.LoggerFactory).Build(); // Load native plugins var plugin = new SearchUrlPlugin(); diff --git a/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs b/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs index 17b9bccf41..7dc25a40f9 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs @@ -177,7 +177,7 @@ Jane: Goodbye! private static IKernel InitializeKernel() { - IKernel kernel = Kernel.Builder + IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureChatCompletionService( TestConfiguration.AzureOpenAI.ChatDeploymentName, diff --git a/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs b/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs index f5136b56cc..b9afec0649 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs @@ -141,7 +141,7 @@ public static class Example15_TextMemoryPlugin private static async Task RunWithStoreAsync(IMemoryStore memoryStore, CancellationToken cancellationToken) { - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithOpenAITextEmbeddingGenerationService(TestConfiguration.OpenAI.EmbeddingModelId, TestConfiguration.OpenAI.ApiKey) diff --git a/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs b/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs index 38e6101cb0..88e8b20b27 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs @@ -44,7 +44,7 @@ public static class Example40_DIContainer //Registering Kernel collection.AddTransient((serviceProvider) => { - return Kernel.Builder + return new KernelBuilder() .WithLoggerFactory(serviceProvider.GetRequiredService()) .WithOpenAITextCompletionService(TestConfiguration.OpenAI.ModelId, TestConfiguration.OpenAI.ApiKey) .Build(); diff --git a/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs b/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs index 1170eb338c..c1fb70e9c5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs @@ -32,7 +32,7 @@ public static class Example41_HttpClientUsage /// private static void UseDefaultHttpClient() { - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithOpenAIChatCompletionService( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey) // If you need to use the default HttpClient from the SK SDK, simply omit the argument for the httpMessageInvoker parameter. @@ -47,7 +47,7 @@ public static class Example41_HttpClientUsage using var httpClient = new HttpClient(); // If you need to use a custom HttpClient, simply pass it as an argument for the httpClient parameter. - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithOpenAIChatCompletionService( modelId: TestConfiguration.OpenAI.ModelId, apiKey: TestConfiguration.OpenAI.ApiKey, @@ -68,7 +68,7 @@ public static class Example41_HttpClientUsage { var factory = sp.GetRequiredService(); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithOpenAIChatCompletionService( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey, @@ -99,7 +99,7 @@ public static class Example41_HttpClientUsage { var factory = sp.GetRequiredService(); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithOpenAIChatCompletionService( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey, diff --git a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs index f90c1ad6d9..6638b78f70 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs @@ -2,7 +2,7 @@ // ========================================================================================================== // The easier way to instantiate the Semantic Kernel is to use KernelBuilder. -// You can access the builder using either Kernel.Builder or KernelBuilder. +// You can access the builder using new KernelBuilder(). #pragma warning disable CA1852 @@ -39,20 +39,20 @@ public static class Example42_KernelBuilder string azureOpenAIEmbeddingDeployment = TestConfiguration.AzureOpenAIEmbeddings.DeploymentName; #pragma warning disable CA1852 // Seal internal types - IKernel kernel1 = Kernel.Builder.Build(); + IKernel kernel1 = new KernelBuilder().Build(); #pragma warning restore CA1852 // Seal internal types - IKernel kernel2 = Kernel.Builder.Build(); + IKernel kernel2 = new KernelBuilder().Build(); // ========================================================================================================== - // Kernel.Builder returns a new builder instance, in case you want to configure the builder differently. + // new KernelBuilder() returns a new builder instance, in case you want to configure the builder differently. // The following are 3 distinct builder instances. var builder1 = new KernelBuilder(); - var builder2 = Kernel.Builder; + var builder2 = new KernelBuilder(); - var builder3 = Kernel.Builder; + var builder3 = new KernelBuilder(); // ========================================================================================================== // A builder instance can create multiple kernel instances, e.g. in case you need @@ -108,7 +108,7 @@ public static class Example42_KernelBuilder // ========================================================================================================== // The AI services are defined with the builder - var kernel7 = Kernel.Builder + var kernel7 = new KernelBuilder() .WithAzureChatCompletionService( deploymentName: azureOpenAIChatCompletionDeployment, endpoint: azureOpenAIEndpoint, @@ -121,7 +121,7 @@ public static class Example42_KernelBuilder // The default behavior can be configured or a custom retry handler can be injected that will apply to all // AI requests (when using the kernel). - var kernel8 = Kernel.Builder.WithRetryBasic( + var kernel8 = new KernelBuilder().WithRetryBasic( new BasicRetryConfig { MaxRetryCount = 3, @@ -147,11 +147,11 @@ public static class Example42_KernelBuilder (ex, timespan, retryCount, _) => logger?.LogWarning(ex, "Error executing action [attempt {RetryCount} of 3], pausing {PausingMilliseconds}ms", retryCount, timespan.TotalMilliseconds)); - var kernel9 = Kernel.Builder.WithHttpHandlerFactory(new PollyHttpRetryHandlerFactory(retryThreeTimesPolicy)).Build(); + var kernel9 = new KernelBuilder().WithHttpHandlerFactory(new PollyHttpRetryHandlerFactory(retryThreeTimesPolicy)).Build(); - var kernel10 = Kernel.Builder.WithHttpHandlerFactory(new PollyRetryThreeTimesFactory()).Build(); + var kernel10 = new KernelBuilder().WithHttpHandlerFactory(new PollyRetryThreeTimesFactory()).Build(); - var kernel11 = Kernel.Builder.WithHttpHandlerFactory(new MyCustomHandlerFactory()).Build(); + var kernel11 = new KernelBuilder().WithHttpHandlerFactory(new MyCustomHandlerFactory()).Build(); return Task.CompletedTask; } diff --git a/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs b/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs index cd5c47e629..2bc4ecfe8f 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs @@ -59,7 +59,7 @@ public static class Example52_ApimAuth .AddConsole(); }); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithLoggerFactory(loggerFactory) .WithAIService(TestConfiguration.AzureOpenAI.ChatDeploymentName, (loggerFactory) => new AzureChatCompletion(TestConfiguration.AzureOpenAI.ChatDeploymentName, openAIClient, loggerFactory)) diff --git a/dotnet/samples/KernelSyntaxExamples/Example56_TemplateNativeFunctionsWithMultipleArguments.cs b/dotnet/samples/KernelSyntaxExamples/Example56_TemplateNativeFunctionsWithMultipleArguments.cs index dd2a557e5f..2b3810405f 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example56_TemplateNativeFunctionsWithMultipleArguments.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example56_TemplateNativeFunctionsWithMultipleArguments.cs @@ -30,7 +30,7 @@ public static class Example56_TemplateNativeFunctionsWithMultipleArguments return; } - IKernel kernel = Kernel.Builder + IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureChatCompletionService( deploymentName: deploymentName, diff --git a/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureRequestSettings.cs b/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureRequestSettings.cs index 7c8f75e521..8afb3b10f4 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureRequestSettings.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureRequestSettings.cs @@ -29,7 +29,7 @@ public static class Example58_ConfigureRequestSettings return; } - IKernel kernel = Kernel.Builder + IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureChatCompletionService( deploymentName: chatDeploymentName, diff --git a/dotnet/src/Connectors/Connectors.Memory.Chroma/README.md b/dotnet/src/Connectors/Connectors.Memory.Chroma/README.md index 22706f210e..a505f2001d 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Chroma/README.md +++ b/dotnet/src/Connectors/Connectors.Memory.Chroma/README.md @@ -25,7 +25,7 @@ const string endpoint = "http://localhost:8000"; ChromaMemoryStore memoryStore = new(endpoint); -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLogger(logger) .WithOpenAITextEmbeddingGenerationService("text-embedding-ada-002", "OPENAI_API_KEY") .WithMemoryStorage(memoryStore) diff --git a/dotnet/src/Connectors/Connectors.Memory.Kusto/README.md b/dotnet/src/Connectors/Connectors.Memory.Kusto/README.md index 6b33ab53ec..6238639ceb 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Kusto/README.md +++ b/dotnet/src/Connectors/Connectors.Memory.Kusto/README.md @@ -14,7 +14,7 @@ using Kusto.Data; var connectionString = new KustoConnectionStringBuilder("https://kvc123.eastus.kusto.windows.net").WithAadUserPromptAuthentication(); KustoMemoryStore memoryStore = new(connectionString, "MyDatabase"); -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLogger(ConsoleLogger.Log) .WithOpenAITextCompletionService(modelId: TestConfiguration.OpenAI.ModelId, apiKey: TestConfiguration.OpenAI.ApiKey) .WithOpenAITextEmbeddingGenerationService(modelId: TestConfiguration.OpenAI.EmbeddingModelId,apiKey: TestConfiguration.OpenAI.ApiKey) diff --git a/dotnet/src/Connectors/Connectors.Memory.Milvus/README.md b/dotnet/src/Connectors/Connectors.Memory.Milvus/README.md index a4aa4d35d9..d88a2aaffd 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Milvus/README.md +++ b/dotnet/src/Connectors/Connectors.Memory.Milvus/README.md @@ -23,7 +23,7 @@ docker-compose up -d ```csharp using MilvusMemoryStore memoryStore = new("localhost"); -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLogger(logger) .WithOpenAITextEmbeddingGenerationService("text-embedding-ada-002", "OPENAI_API_KEY") .WithMemoryStorage(memoryStore) diff --git a/dotnet/src/Connectors/Connectors.Memory.Postgres/README.md b/dotnet/src/Connectors/Connectors.Memory.Postgres/README.md index f1ccc1f8b0..937e03a039 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Postgres/README.md +++ b/dotnet/src/Connectors/Connectors.Memory.Postgres/README.md @@ -41,7 +41,7 @@ NpgsqlDataSource dataSource = dataSourceBuilder.Build(); PostgresMemoryStore memoryStore = new PostgresMemoryStore(dataSource, vectorSize: 1536/*, schema: "public" */); -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLogger(ConsoleLogger.Logger) .WithOpenAITextEmbeddingGenerationService("text-embedding-ada-002", Env.Var("OPENAI_API_KEY")) .WithMemoryStorage(memoryStore) diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/README.md b/dotnet/src/Connectors/Connectors.Memory.Redis/README.md index 3827e94d2d..77ff2e8559 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/README.md +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/README.md @@ -30,7 +30,7 @@ ConnectionMultiplexer connectionMultiplexer = await ConnectionMultiplexer.Connec IDatabase database = connectionMultiplexer.GetDatabase(); RedisMemoryStore memoryStore = new RedisMemoryStore(database, vectorSize: 1536); -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLogger(ConsoleLogger.Logger) .WithOpenAITextEmbeddingGenerationService("text-embedding-ada-002", Env.Var("OPENAI_API_KEY")) .WithMemoryStorage(memoryStore) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs index ec88b60717..8ec0c712ad 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs @@ -16,7 +16,7 @@ public class AIServicesOpenAIExtensionsTests [Fact] public void ItSucceedsWhenAddingDifferentServiceTypeWithSameId() { - KernelBuilder targetBuilder = Kernel.Builder; + KernelBuilder targetBuilder = new(); targetBuilder.WithAzureTextCompletionService("depl", "https://url", "key", "azure"); targetBuilder.WithAzureTextEmbeddingGenerationService("depl2", "https://url", "key", "azure"); @@ -28,7 +28,7 @@ public class AIServicesOpenAIExtensionsTests [Fact] public void ItTellsIfAServiceIsAvailable() { - KernelBuilder targetBuilder = Kernel.Builder; + KernelBuilder targetBuilder = new(); targetBuilder.WithAzureTextCompletionService("depl", "https://url", "key", serviceId: "azure"); targetBuilder.WithOpenAITextCompletionService("model", "apikey", serviceId: "oai"); targetBuilder.WithAzureTextEmbeddingGenerationService("depl2", "https://url2", "key", serviceId: "azure"); @@ -46,7 +46,7 @@ public class AIServicesOpenAIExtensionsTests public void ItCanOverwriteServices() { // Arrange - KernelBuilder targetBuilder = Kernel.Builder; + KernelBuilder targetBuilder = new(); // Act - Assert no exception occurs targetBuilder.WithAzureTextCompletionService("dep", "https://localhost", "key", serviceId: "one"); diff --git a/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs b/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs index 5109a388c9..f118965bfa 100644 --- a/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs +++ b/dotnet/src/IntegrationTests/Extensions/KernelSemanticFunctionExtensionsTests.cs @@ -27,7 +27,7 @@ public sealed class KernelSemanticFunctionExtensionsTests : IDisposable [Fact] public async Task ItSupportsFunctionCallsAsync() { - var builder = Kernel.Builder + var builder = new KernelBuilder() .WithAIService(null, new RedirectTextCompletion(), true) .WithLoggerFactory(this._logger); IKernel target = builder.Build(); @@ -46,7 +46,7 @@ public sealed class KernelSemanticFunctionExtensionsTests : IDisposable [Fact] public async Task ItSupportsFunctionCallsWithInputAsync() { - var builder = Kernel.Builder + var builder = new KernelBuilder() .WithAIService(null, new RedirectTextCompletion(), true) .WithLoggerFactory(this._logger); IKernel target = builder.Build(); diff --git a/dotnet/src/IntegrationTests/Planners/PlanTests.cs b/dotnet/src/IntegrationTests/Planners/PlanTests.cs index b4954fb91e..4cbd03ab73 100644 --- a/dotnet/src/IntegrationTests/Planners/PlanTests.cs +++ b/dotnet/src/IntegrationTests/Planners/PlanTests.cs @@ -507,7 +507,7 @@ public sealed class PlanTests : IDisposable AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - var builder = Kernel.Builder + var builder = new KernelBuilder() .WithLoggerFactory(this._loggerFactory) .WithRetryBasic(); diff --git a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs b/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs index b894666e94..b16fd1fe6e 100644 --- a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs +++ b/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs @@ -32,7 +32,7 @@ public class SequentialPlanParserTests AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); Assert.NotNull(azureOpenAIConfiguration); - IKernel kernel = Kernel.Builder + IKernel kernel = new KernelBuilder() .WithRetryBasic() .WithAzureTextCompletionService( deploymentName: azureOpenAIConfiguration.DeploymentName, diff --git a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs b/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs index 6236f64e4c..facf6f5cfc 100644 --- a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs +++ b/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs @@ -117,7 +117,7 @@ public sealed class SequentialPlannerTests : IDisposable AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - var builder = Kernel.Builder.WithLoggerFactory(this._logger); + var builder = new KernelBuilder().WithLoggerFactory(this._logger); builder.WithRetryBasic(); if (useChatModel) diff --git a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs b/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs index c99ab62ec7..79881ed0e7 100644 --- a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs +++ b/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs @@ -147,7 +147,7 @@ public sealed class StepwisePlannerTests : IDisposable AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - var builder = Kernel.Builder + var builder = new KernelBuilder() .WithLoggerFactory(this._loggerFactory) .WithRetryBasic(); diff --git a/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs b/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs index 2bbd8220e3..5c68ca4d34 100644 --- a/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs +++ b/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs @@ -32,7 +32,7 @@ public sealed class PromptTemplateEngineTests : IDisposable const string Winner = "SK"; const string Template = "And the winner\n of {{$input}} \nis: {{ $winner }}!"; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var context = kernel.CreateNewContext(); context.Variables["input"] = Input; context.Variables["winner"] = Winner; @@ -54,7 +54,7 @@ public sealed class PromptTemplateEngineTests : IDisposable const string Template = "And the winner\n of {{'template\ntests'}} \nis: {{ \"SK\" }}!"; const string Expected = "And the winner\n of template\ntests \nis: SK!"; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var context = kernel.CreateNewContext(); // Act @@ -69,7 +69,7 @@ public sealed class PromptTemplateEngineTests : IDisposable { // Arrange const string Template = "== {{my.check123 $call}} =="; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); kernel.ImportFunctions(new MyPlugin(), "my"); var context = kernel.CreateNewContext(); context.Variables["call"] = "123"; @@ -86,7 +86,7 @@ public sealed class PromptTemplateEngineTests : IDisposable { // Arrange const string Template = "== {{my.check123 '234'}} =="; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); kernel.ImportFunctions(new MyPlugin(), "my"); var context = kernel.CreateNewContext(); @@ -103,7 +103,7 @@ public sealed class PromptTemplateEngineTests : IDisposable // Arrange const char Esc = '\\'; string template = "== {{my.check123 'a" + Esc + "'b'}} =="; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); kernel.ImportFunctions(new MyPlugin(), "my"); var context = kernel.CreateNewContext(); @@ -120,7 +120,7 @@ public sealed class PromptTemplateEngineTests : IDisposable // Arrange const char Esc = '\\'; string template = "== {{my.check123 \"a" + Esc + "\"b\"}} =="; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); kernel.ImportFunctions(new MyPlugin(), "my"); var context = kernel.CreateNewContext(); @@ -136,7 +136,7 @@ public sealed class PromptTemplateEngineTests : IDisposable { // Arrange string template = "Output: {{my.sayAge name=\"Mario\" birthdate=$birthdate exclamation='Wow, that\\'s surprising'}}"; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); kernel.ImportFunctions(new MyPlugin(), "my"); var context = kernel.CreateNewContext(); context.Variables["birthdate"] = "1981-08-20T00:00:00"; @@ -153,7 +153,7 @@ public sealed class PromptTemplateEngineTests : IDisposable public async Task ItHandleEdgeCasesAsync(string template, string expectedResult) { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); kernel.ImportFunctions(new MyPlugin()); // Act diff --git a/dotnet/src/IntegrationTests/WebPlugin/WebPluginTests.cs b/dotnet/src/IntegrationTests/WebPlugin/WebPluginTests.cs index 8f35c973c6..571078785f 100644 --- a/dotnet/src/IntegrationTests/WebPlugin/WebPluginTests.cs +++ b/dotnet/src/IntegrationTests/WebPlugin/WebPluginTests.cs @@ -43,7 +43,7 @@ public sealed class WebPluginTests : IDisposable public async Task BingPluginTestAsync(string prompt, string expectedAnswerContains) { // Arrange - IKernel kernel = Kernel.Builder.WithLoggerFactory(this._logger).Build(); + IKernel kernel = new KernelBuilder().WithLoggerFactory(this._logger).Build(); using XunitLogger connectorLogger = new(this._output); BingConnector connector = new(this._bingApiKey, connectorLogger); @@ -66,7 +66,7 @@ public sealed class WebPluginTests : IDisposable public async Task WebFileDownloadPluginFileTestAsync() { // Arrange - IKernel kernel = Kernel.Builder.WithLoggerFactory(this._logger).Build(); + IKernel kernel = new KernelBuilder().WithLoggerFactory(this._logger).Build(); using XunitLogger pluginLogger = new(this._output); var plugin = new WebFileDownloadPlugin(pluginLogger); var downloadFunctions = kernel.ImportFunctions(plugin, "WebFileDownload"); diff --git a/dotnet/src/InternalUtilities/test/FunctionHelpers.cs b/dotnet/src/InternalUtilities/test/FunctionHelpers.cs index 0057c0f027..8a1c6638a3 100644 --- a/dotnet/src/InternalUtilities/test/FunctionHelpers.cs +++ b/dotnet/src/InternalUtilities/test/FunctionHelpers.cs @@ -18,7 +18,7 @@ internal static class FunctionHelpers string methodName, params (string Name, object Value)[] variables) { - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); IDictionary functions = kernel.ImportFunctions(pluginInstance); diff --git a/dotnet/src/Plugins/Plugins.Core/ConversationSummaryPlugin.cs b/dotnet/src/Plugins/Plugins.Core/ConversationSummaryPlugin.cs index 633e50f1c4..01fca19a6e 100644 --- a/dotnet/src/Plugins/Plugins.Core/ConversationSummaryPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Core/ConversationSummaryPlugin.cs @@ -14,7 +14,7 @@ namespace Microsoft.SemanticKernel.Plugins.Core; /// /// /// -/// var kernel Kernel.Builder.Build(); +/// var kernel = new KernelBuilder().Build(); /// kernel.ImportFunctions(new ConversationSummaryPlugin(kernel)); /// /// diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs index 3ba1750aa7..45e8aa69c1 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs @@ -22,7 +22,7 @@ public class FileIOPluginTests public void ItCanBeImported() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act var functions = kernel.ImportFunctions(new FileIOPlugin(), "fileIO"); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/MathPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/MathPluginTests.cs index 7cd2f2334d..184c9890c5 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/MathPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/MathPluginTests.cs @@ -22,7 +22,7 @@ public class MathPluginTests public void ItCanBeImported() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act - Assert no exception occurs e.g. due to reflection kernel.ImportFunctions(new MathPlugin(), "math"); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/TextPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/TextPluginTests.cs index 268140be23..80d683fb95 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/TextPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/TextPluginTests.cs @@ -19,7 +19,7 @@ public class TextPluginTests public void ItCanBeImported() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act - Assert no exception occurs e.g. due to reflection kernel.ImportFunctions(new TextPlugin(), "text"); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs index a066612b35..f4b85f1b98 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs @@ -25,7 +25,7 @@ public class TimePluginTests public void ItCanBeImported() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act - Assert no exception occurs e.g. due to reflection kernel.ImportFunctions(new TimePlugin(), "time"); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/WaitPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/WaitPluginTests.cs index 4cc8a472b3..a6d23b1fb4 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/WaitPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/WaitPluginTests.cs @@ -24,7 +24,7 @@ public class WaitPluginTests public void ItCanBeImported() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act - Assert no exception occurs e.g. due to reflection kernel.ImportFunctions(new WaitPlugin(), "wait"); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Web/SearchUrlSkillTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Web/SearchUrlSkillTests.cs index 8a47bfba53..2e74c54d48 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Web/SearchUrlSkillTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Web/SearchUrlSkillTests.cs @@ -23,7 +23,7 @@ public class SearchUrlPluginTests public void ItCanBeImported() { // Arrange - IKernel kernel = Kernel.Builder.Build(); + IKernel kernel = new KernelBuilder().Build(); // Act - Assert no exception occurs e.g. due to reflection kernel.ImportFunctions(new SearchUrlPlugin(), "search"); diff --git a/dotnet/src/SemanticKernel.Core/Kernel.cs b/dotnet/src/SemanticKernel.Core/Kernel.cs index d7be7c73a7..3999733678 100644 --- a/dotnet/src/SemanticKernel.Core/Kernel.cs +++ b/dotnet/src/SemanticKernel.Core/Kernel.cs @@ -44,6 +44,7 @@ public sealed class Kernel : IKernel, IDisposable /// /// Return a new instance of the kernel builder, used to build and configure kernel instances. /// + [Obsolete("This field will be removed in a future release. Initialize KernelBuilder through constructor instead (new KernelBuilder()).")] public static KernelBuilder Builder => new(); /// diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs index 23fb743e9c..fbe791c58a 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/MultipleModelTests.cs @@ -25,7 +25,7 @@ public class MultipleModelTests mockTextCompletion2.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, false) .WithAIService("service2", mockTextCompletion2.Object, true) .Build(); @@ -49,7 +49,7 @@ public class MultipleModelTests var mockTextCompletion1 = new Mock(); var mockTextCompletion2 = new Mock(); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, false) .WithAIService("service2", mockTextCompletion2.Object, true) .Build(); @@ -83,7 +83,7 @@ public class MultipleModelTests mockTextCompletion3.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, defaultServiceIndex == 0) .WithAIService("service2", mockTextCompletion2.Object, defaultServiceIndex == 1) .WithAIService("service3", mockTextCompletion3.Object, defaultServiceIndex == 2) @@ -119,7 +119,7 @@ public class MultipleModelTests mockTextCompletion3.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, true) .WithAIService("service2", mockTextCompletion2.Object, false) .WithAIService("service3", mockTextCompletion3.Object, false) diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests3.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests3.cs index 89c3801239..b114016bac 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests3.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests3.cs @@ -42,7 +42,7 @@ public sealed class SKFunctionTests3 .Where(m => m.Name is not "GetType" and not "Equals" and not "GetHashCode" and not "ToString") .ToArray(); - ISKFunction[] functions = Kernel.Builder.Build().ImportFunctions(pluginInstance).Select(s => s.Value).ToArray(); + ISKFunction[] functions = new KernelBuilder().Build().ImportFunctions(pluginInstance).Select(s => s.Value).ToArray(); // Act Assert.Equal(methods.Length, functions.Length); @@ -81,7 +81,7 @@ public sealed class SKFunctionTests3 public async Task ItCanImportNativeFunctionsAsync() { // Arrange - var context = Kernel.Builder.Build().CreateNewContext(); + var context = new KernelBuilder().Build().CreateNewContext(); context.Variables["done"] = "NO"; // Note: the function doesn't have any SK attributes @@ -113,7 +113,7 @@ public sealed class SKFunctionTests3 public async Task ItCanImportNativeFunctionsWithExternalReferencesAsync() { // Arrange - var context = Kernel.Builder.Build().CreateNewContext(); + var context = new KernelBuilder().Build().CreateNewContext(); context.Variables["done"] = "NO"; // Note: This is an important edge case that affects the function signature and how delegates diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs index 4de7067cf1..d04cc759fc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SemanticFunctionTests.cs @@ -27,7 +27,7 @@ public class SemanticFunctionTests { // Arrange var factory = new Mock>(); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithDefaultAIService(factory.Object) .Build(); @@ -50,7 +50,7 @@ public class SemanticFunctionTests mockTextCompletion.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("x", mockTextCompletion.Object) .Build(); @@ -73,7 +73,7 @@ public class SemanticFunctionTests public void ItAllowsToCreateFunctionsInTheGlobalNamespace() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var templateConfig = new PromptTemplateConfig(); // Act @@ -95,7 +95,7 @@ public class SemanticFunctionTests mockTextCompletion2.Setup(c => c.GetCompletionsAsync(It.IsAny(), null, It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, false) .WithAIService("service2", mockTextCompletion2.Object, true) .Build(); @@ -123,7 +123,7 @@ public class SemanticFunctionTests mockTextCompletion2.Setup(c => c.GetCompletionsAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new[] { mockCompletionResult.Object }); mockCompletionResult.Setup(cr => cr.GetCompletionAsync(It.IsAny())).ReturnsAsync("llmResult"); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, false) .WithAIService("service2", mockTextCompletion2.Object, true) .Build(); @@ -147,7 +147,7 @@ public class SemanticFunctionTests var mockTextCompletion1 = new Mock(); var mockTextCompletion2 = new Mock(); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithAIService("service1", mockTextCompletion1.Object, false) .WithAIService("service2", mockTextCompletion2.Object, true) .Build(); @@ -170,7 +170,7 @@ public class SemanticFunctionTests { // Arrange var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var sut = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var invoked = 0; @@ -196,7 +196,7 @@ public class SemanticFunctionTests public async Task RunAsyncHandlesPreInvocationWasCancelledAsync() { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var input = "Test input"; var invoked = false; @@ -219,7 +219,7 @@ public class SemanticFunctionTests { // Arrange var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var sut = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var invoked = 0; @@ -241,7 +241,7 @@ public class SemanticFunctionTests public async Task RunAsyncPreInvocationCancelationDontTriggerInvokedHandlerAsync() { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var invoked = 0; @@ -267,7 +267,7 @@ public class SemanticFunctionTests { // Arrange var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var sut = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var semanticFunction1 = sut.CreateSemanticFunction("Write one phrase about UnitTests", functionName: "SkipMe"); var semanticFunction2 = sut.CreateSemanticFunction("Write two phrases about UnitTests", functionName: "DontSkipMe"); var invoked = 0; @@ -307,7 +307,7 @@ public class SemanticFunctionTests { // Arrange var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var sut = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var semanticFunction = sut.CreateSemanticFunction("Write a simple phrase about UnitTests"); var invoked = 0; @@ -335,7 +335,7 @@ public class SemanticFunctionTests public async Task RunAsyncChangeVariableInvokingHandlerAsync() { var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var sut = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var prompt = "Write a simple phrase about UnitTests {{$input}}"; var semanticFunction = sut.CreateSemanticFunction(prompt); @@ -358,7 +358,7 @@ public class SemanticFunctionTests public async Task RunAsyncChangeVariableInvokedHandlerAsync() { var (mockTextResult, mockTextCompletion) = this.SetupMocks(); - var sut = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var sut = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var prompt = "Write a simple phrase about UnitTests {{$input}}"; var semanticFunction = sut.CreateSemanticFunction(prompt); @@ -391,7 +391,7 @@ public class SemanticFunctionTests const string Prompt = "Write a simple phrase about UnitTests"; var (mockTextResult, mockTextCompletion) = this.SetupMocks("Result3"); - var kernel = Kernel.Builder.WithAIService(null, mockTextCompletion.Object).Build(); + var kernel = new KernelBuilder().WithAIService(null, mockTextCompletion.Object).Build(); var function1 = SKFunction.FromNativeMethod(Method(Function1), pluginName: PluginName); var function2 = SKFunction.FromNativeMethod(Method(Function2), pluginName: PluginName); diff --git a/dotnet/src/SemanticKernel.UnitTests/KernelTests.cs b/dotnet/src/SemanticKernel.UnitTests/KernelTests.cs index dd41db6af4..3629853742 100644 --- a/dotnet/src/SemanticKernel.UnitTests/KernelTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/KernelTests.cs @@ -27,7 +27,7 @@ public class KernelTests { // Arrange var factory = new Mock>(); - var kernel = Kernel.Builder + var kernel = new KernelBuilder() .WithDefaultAIService(factory.Object) .Build(); @@ -45,7 +45,7 @@ public class KernelTests public async Task RunAsyncDoesNotRunWhenCancelledAsync() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var nativePlugin = new MyPlugin(); var functions = kernel.ImportFunctions(nativePlugin, "mySk"); @@ -60,7 +60,7 @@ public class KernelTests public async Task RunAsyncRunsWhenNotCancelledAsync() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var nativePlugin = new MyPlugin(); kernel.ImportFunctions(nativePlugin, "mySk"); @@ -77,7 +77,7 @@ public class KernelTests public void ItImportsPluginsNotCaseSensitive() { // Act - IDictionary functions = Kernel.Builder.Build().ImportFunctions(new MyPlugin(), "test"); + IDictionary functions = new KernelBuilder().Build().ImportFunctions(new MyPlugin(), "test"); // Assert Assert.Equal(3, functions.Count); @@ -90,7 +90,7 @@ public class KernelTests public void ItAllowsToImportFunctionsInTheGlobalNamespace() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act IDictionary functions = kernel.ImportFunctions(new MyPlugin()); @@ -105,7 +105,7 @@ public class KernelTests public void ItAllowsToImportTheSamePluginMultipleTimes() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Act - Assert no exception occurs kernel.ImportFunctions(new MyPlugin()); @@ -119,7 +119,7 @@ public class KernelTests public async Task RunAsyncHandlesPreInvocationAsync(int pipelineCount) { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var myPlugin = new Mock(); var functions = sut.ImportFunctions(myPlugin.Object, "MyPlugin"); @@ -146,7 +146,7 @@ public class KernelTests public async Task RunAsyncHandlesPreInvocationWasCancelledAsync() { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var functions = sut.ImportFunctions(new MyPlugin(), "MyPlugin"); var invoked = false; @@ -168,7 +168,7 @@ public class KernelTests public async Task RunAsyncHandlesPreInvocationCancelationDontRunSubsequentFunctionsInThePipelineAsync() { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var (mockTextResult, mockTextCompletion) = this.SetupMocks(); var myPlugin = new Mock(); var functions = sut.ImportFunctions(myPlugin.Object, "MyPlugin"); @@ -193,7 +193,7 @@ public class KernelTests public async Task RunAsyncPreInvocationCancelationDontTriggerInvokedHandlerAsync() { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var functions = sut.ImportFunctions(new MyPlugin(), "MyPlugin"); var invoked = 0; @@ -218,7 +218,7 @@ public class KernelTests public async Task RunAsyncPreInvocationSkipDontTriggerInvokedHandlerAsync() { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var (mockTextResult, mockTextCompletion) = this.SetupMocks(); var myPlugin = new Mock(); var functions = sut.ImportFunctions(myPlugin.Object, "MyPlugin"); @@ -258,7 +258,7 @@ public class KernelTests public async Task RunAsyncHandlesPostInvocationAsync(int pipelineCount) { // Arrange - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var myPlugin = new Mock(); var functions = sut.ImportFunctions(myPlugin.Object, "MyPlugin"); @@ -285,7 +285,7 @@ public class KernelTests [Fact] public async Task RunAsyncChangeVariableInvokingHandlerAsync() { - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var myPlugin = new Mock(); var functions = sut.ImportFunctions(myPlugin.Object, "MyPlugin"); @@ -307,7 +307,7 @@ public class KernelTests [Fact] public async Task RunAsyncChangeVariableInvokedHandlerAsync() { - var sut = Kernel.Builder.Build(); + var sut = new KernelBuilder().Build(); var myPlugin = new Mock(); var functions = sut.ImportFunctions(myPlugin.Object, "MyPlugin"); @@ -338,7 +338,7 @@ public class KernelTests const string PluginName = "MyPlugin"; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var function1 = SKFunction.FromNativeMethod(Method(Function1), pluginName: PluginName); var function2 = SKFunction.FromNativeMethod(Method(Function2), pluginName: PluginName); @@ -358,7 +358,7 @@ public class KernelTests [Fact] public async Task ItReturnsChangedResultsFromFunctionInvokedEventsAsync() { - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); // Arrange [SKName("Function1")] @@ -385,7 +385,7 @@ public class KernelTests public async Task ItReturnsChangedResultsFromFunctionInvokingEventsAsync() { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); [SKName("Function1")] static string Function1(SKContext context) => context.Variables["injected variable"]; @@ -413,7 +413,7 @@ public class KernelTests public async Task ItRepeatsFunctionInvokedEventsAsync(string retryFunction, int numberOfRepeats) { // Arrange - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); [SKName("Function1")] static string Function1(SKContext context) => "Result1"; @@ -464,7 +464,7 @@ public class KernelTests const string PluginName = "MyPlugin"; - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); var function1 = SKFunction.FromNativeMethod(Method(Function1), pluginName: PluginName); var function2 = SKFunction.FromNativeMethod(Method(Function2), pluginName: PluginName); @@ -528,7 +528,7 @@ public class KernelTests static string Function5() => "Result5"; functions.Add(SKFunction.FromNativeMethod(Method(Function5), pluginName: PluginName)); - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); int numberOfInvocations = 0; @@ -587,7 +587,7 @@ public class KernelTests static string Function5() => "Result5"; functions.Add(SKFunction.FromNativeMethod(Method(Function5), pluginName: PluginName)); - var kernel = Kernel.Builder.Build(); + var kernel = new KernelBuilder().Build(); int numberOfInvocations = 0; diff --git a/samples/dotnet/KernelHttpServer/SemanticKernelFactory.cs b/samples/dotnet/KernelHttpServer/SemanticKernelFactory.cs index 81b74cf888..33d2104baf 100644 --- a/samples/dotnet/KernelHttpServer/SemanticKernelFactory.cs +++ b/samples/dotnet/KernelHttpServer/SemanticKernelFactory.cs @@ -36,7 +36,7 @@ internal static class SemanticKernelFactory logger.LogWarning("Text embedding service has not been supplied"); } - KernelBuilder builder = Kernel.Builder; + KernelBuilder builder = new(); builder = _ConfigureKernelBuilder(apiConfig, builder, memoryStore); return _CompleteKernelSetup(req, builder, logger, skillsToLoad); } diff --git a/samples/dotnet/MsGraphPluginsExample/Program.cs b/samples/dotnet/MsGraphPluginsExample/Program.cs index bdecc9e736..d51ac1cf22 100644 --- a/samples/dotnet/MsGraphPluginsExample/Program.cs +++ b/samples/dotnet/MsGraphPluginsExample/Program.cs @@ -107,7 +107,7 @@ public sealed class Program EmailSkill outlookSkill = new(new OutlookMailConnector(graphServiceClient), loggerFactory.CreateLogger()); // Initialize the Semantic Kernel and and register connections with OpenAI/Azure OpenAI instances. - KernelBuilder builder = Kernel.Builder + KernelBuilder builder = new KernelBuilder() .WithLogger(loggerFactory.CreateLogger()); if (configuration.GetSection("AzureOpenAI:ServiceId").Value != null) diff --git a/samples/dotnet/OpenApiPluginsExample/Program.cs b/samples/dotnet/OpenApiPluginsExample/Program.cs index c8fa61389f..8ceb563b82 100644 --- a/samples/dotnet/OpenApiPluginsExample/Program.cs +++ b/samples/dotnet/OpenApiPluginsExample/Program.cs @@ -51,22 +51,22 @@ internal sealed class Program AIServiceOptions aiOptions = configuration.GetRequiredSection(AIServiceOptions.PropertyName).Get() ?? throw new InvalidOperationException($"Missing configuration for {AIServiceOptions.PropertyName}."); - KernelBuilder kb = Kernel.Builder + KernelBuilder builder = new KernelBuilder() .WithLogger(loggerFactory.CreateLogger()); switch (aiOptions.Type) { case AIServiceOptions.AIServiceType.AzureOpenAI: - kb.WithAzureChatCompletionService(aiOptions.Models.Completion, aiOptions.Endpoint, aiOptions.Key); + builder.WithAzureChatCompletionService(aiOptions.Models.Completion, aiOptions.Endpoint, aiOptions.Key); break; case AIServiceOptions.AIServiceType.OpenAI: - kb.WithOpenAIChatCompletionService(aiOptions.Models.Completion, aiOptions.Key); + builder.WithOpenAIChatCompletionService(aiOptions.Models.Completion, aiOptions.Key); break; default: throw new InvalidOperationException($"Unhandled AI service type {aiOptions.Type}"); } - var kernel = kb.Build(); + var kernel = builder.Build(); // Register the GitHub plugin using an OpenAPI definition containing only pull request GET operations. GitHubPluginOptions gitHubOptions = configuration.GetRequiredSection(GitHubPluginOptions.PropertyName).Get() From 0c1b669d234767c9a3b4839c8859e2ec548780cb Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:16:27 +0100 Subject: [PATCH 06/24] .Net: Plans using FunctionRunner + RequestSettings (#3264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation and Context Plans now fully utilize Kernel.RunAsync thru FunctionRunner interface, this will allow Plan steps to trigger Pre/Post Hooks defined in the Kernel that the Plan was ran. Option to override the RequestSettings in the Kernel.RunAsync call. This is was required while running Plan steps that by default will use the current Plan settings. One more step towards using RunAsync as the main entry point + Pre/Post hooks phase 2. Resolves Partially #2324 ### Description - Kernel.RunAsync optionally accepts `AIRequestSettings` - PlanSteps now run in the Kernel and can trigger Pre/Post Hooks  ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- .../Sequential/SequentialPlannerTests.cs | 8 ++--- .../SemanticKernel.Abstractions/IKernel.cs | 5 +++- .../Orchestration/FunctionRunnerExtensions.cs | 29 +++++++++++++++++++ .../Orchestration/IFunctionRunner.cs | 3 ++ dotnet/src/SemanticKernel.Core/Kernel.cs | 5 ++-- .../SemanticKernel.Core/KernelExtensions.cs | 27 ++++++++++++++--- .../Orchestration/FunctionRunner.cs | 11 +++++-- .../src/SemanticKernel.Core/Planning/Plan.cs | 12 +++++--- .../Planning/PlanSerializationTests.cs | 17 ++++++----- .../Planning/PlanTests.cs | 12 ++++---- 10 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs index 200f4088d9..75af7b4ae9 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs @@ -22,8 +22,8 @@ public sealed class SequentialPlannerTests // Arrange var kernel = new Mock(); kernel.Setup(x => x.LoggerFactory).Returns(new Mock().Object); - kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (vars, cancellationToken, functions) => + kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (vars, requestSettings, cancellationToken, functions) => { var functionResult = await functions[0].InvokeAsync(kernel.Object, vars, cancellationToken: cancellationToken); return KernelResult.FromFunctionResults(functionResult.GetValue(), new List { functionResult }); @@ -173,8 +173,8 @@ public sealed class SequentialPlannerTests // Mock Plugins kernel.Setup(x => x.Functions).Returns(functions.Object); - kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (vars, cancellationToken, functions) => + kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (vars, requestSettings, cancellationToken, functions) => { var functionResult = await functions[0].InvokeAsync(kernel.Object, vars, cancellationToken: cancellationToken); return KernelResult.FromFunctionResults(functionResult.GetValue(), new List { functionResult }); diff --git a/dotnet/src/SemanticKernel.Abstractions/IKernel.cs b/dotnet/src/SemanticKernel.Abstractions/IKernel.cs index 29b10e9e1f..c0a17815bf 100644 --- a/dotnet/src/SemanticKernel.Abstractions/IKernel.cs +++ b/dotnet/src/SemanticKernel.Abstractions/IKernel.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Events; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Memory; @@ -52,12 +53,14 @@ public interface IKernel /// Run a pipeline composed of synchronous and asynchronous functions. /// /// Input to process + /// Request settings to use in the functions call /// The to monitor for cancellation requests. The default is . /// List of functions /// Result of the function composition Task RunAsync( ContextVariables variables, - CancellationToken cancellationToken, + AIRequestSettings? requestSettings = null, + CancellationToken cancellationToken = default, params ISKFunction[] pipeline); /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs new file mode 100644 index 0000000000..97294a2fe2 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Orchestration; + +/// +/// Function runner extensions. +/// +public static class FunctionRunnerExtensions +{ + /// + /// Execute a function using the resources loaded in the context. + /// + /// Target function runner + /// Target function to run + /// Input to process + /// The to monitor for cancellation requests. The default is . + /// Result of the function composition + public static Task RunAsync( + this IFunctionRunner functionRunner, + ISKFunction skFunction, + ContextVariables? variables = null, + CancellationToken cancellationToken = default) + { + return functionRunner.RunAsync(skFunction, variables, null, cancellationToken); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs b/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs index 71357d2f30..87227f6dfd 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.AI; namespace Microsoft.SemanticKernel.Orchestration; @@ -15,11 +16,13 @@ public interface IFunctionRunner /// /// Target function to run /// Input to process + /// Request settings to override /// The to monitor for cancellation requests. The default is . /// Result of the function composition Task RunAsync( ISKFunction skFunction, ContextVariables? variables = null, + AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default); /// diff --git a/dotnet/src/SemanticKernel.Core/Kernel.cs b/dotnet/src/SemanticKernel.Core/Kernel.cs index 3999733678..013774b61b 100644 --- a/dotnet/src/SemanticKernel.Core/Kernel.cs +++ b/dotnet/src/SemanticKernel.Core/Kernel.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Events; using Microsoft.SemanticKernel.Http; @@ -97,7 +98,7 @@ public sealed class Kernel : IKernel, IDisposable } /// - public async Task RunAsync(ContextVariables variables, CancellationToken cancellationToken, params ISKFunction[] pipeline) + public async Task RunAsync(ContextVariables variables, AIRequestSettings? requestSettings, CancellationToken cancellationToken = default, params ISKFunction[] pipeline) { var context = this.CreateNewContext(variables); @@ -128,7 +129,7 @@ repeat: continue; } - functionResult = await skFunction.InvokeAsync(context, cancellationToken: cancellationToken).ConfigureAwait(false); + functionResult = await skFunction.InvokeAsync(context, requestSettings, cancellationToken).ConfigureAwait(false); context = functionResult.Context; diff --git a/dotnet/src/SemanticKernel.Core/KernelExtensions.cs b/dotnet/src/SemanticKernel.Core/KernelExtensions.cs index 41545d15a7..82d57b09d9 100644 --- a/dotnet/src/SemanticKernel.Core/KernelExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/KernelExtensions.cs @@ -85,7 +85,26 @@ public static class KernelExtensions CancellationToken cancellationToken = default) { Verify.NotNull(kernel); - return kernel.RunAsync(variables ?? new(), cancellationToken, skFunction); + return kernel.RunAsync(variables ?? new(), null, cancellationToken, skFunction); + } + + /// + /// Run a pipeline composed of synchronous and asynchronous functions. + /// + /// The kernel. + /// Variables to process + /// Cancellation token + /// List of functions + /// Result of the function composition + public static Task RunAsync( + this IKernel kernel, + ContextVariables variables, + CancellationToken cancellationToken = default, + params ISKFunction[] pipeline) + { + Verify.NotNull(kernel); + + return kernel.RunAsync(variables, null, cancellationToken, pipeline); } /// @@ -131,7 +150,7 @@ public static class KernelExtensions params ISKFunction[] pipeline) { Verify.NotNull(kernel); - return kernel.RunAsync(variables, CancellationToken.None, pipeline); + return kernel.RunAsync(variables, null, CancellationToken.None, pipeline); } /// @@ -147,7 +166,7 @@ public static class KernelExtensions params ISKFunction[] pipeline) { Verify.NotNull(kernel); - return kernel.RunAsync(new ContextVariables(), cancellationToken, pipeline); + return kernel.RunAsync(new ContextVariables(), null, cancellationToken, pipeline); } /// @@ -165,6 +184,6 @@ public static class KernelExtensions params ISKFunction[] pipeline) { Verify.NotNull(kernel); - return kernel.RunAsync(new ContextVariables(input), cancellationToken, pipeline); + return kernel.RunAsync(new ContextVariables(input), null, cancellationToken, pipeline); } } diff --git a/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs b/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs index bb89b166f8..ae9dd77888 100644 --- a/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs +++ b/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.AI; namespace Microsoft.SemanticKernel.Orchestration; @@ -23,9 +24,13 @@ internal class FunctionRunner : IFunctionRunner } /// - public async Task RunAsync(ISKFunction skFunction, ContextVariables? variables = null, CancellationToken cancellationToken = default) + public async Task RunAsync( + ISKFunction skFunction, + ContextVariables? variables = null, + AIRequestSettings? requestSettings = null, + CancellationToken cancellationToken = default) { - return (await this._kernel.RunAsync(skFunction, variables, cancellationToken).ConfigureAwait(false)) + return (await this._kernel.RunAsync(variables ?? new(), requestSettings, cancellationToken, skFunction).ConfigureAwait(false)) .FunctionResults.First(); } @@ -33,6 +38,6 @@ internal class FunctionRunner : IFunctionRunner public Task RunAsync(string pluginName, string functionName, ContextVariables? variables = null, CancellationToken cancellationToken = default) { var function = this._kernel.Functions.GetFunction(pluginName, functionName); - return this.RunAsync(function, variables, cancellationToken); + return this.RunAsync(function, variables, null, cancellationToken); } } diff --git a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs index 46c954ad07..a702d63679 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs @@ -246,7 +246,7 @@ public sealed class Plan : IPlan var functionVariables = this.GetNextStepVariables(context.Variables, step); // Execute the step - var result = await context.Runner.RunAsync(step, functionVariables, cancellationToken).ConfigureAwait(false); + var result = await context.Runner.RunAsync(step, functionVariables, null, cancellationToken).ConfigureAwait(false); var resultValue = result.Context.Result.Trim(); @@ -336,10 +336,14 @@ public sealed class Plan : IPlan var functionContext = context.Clone(functionVariables, context.Functions); // Execute the step - result = await this.Function - .WithInstrumentation(context.LoggerFactory) - .InvokeAsync(functionContext, requestSettings, cancellationToken) + + result = await context.Runner.RunAsync( + this.Function.WithInstrumentation(context.LoggerFactory), + functionVariables, + requestSettings, + cancellationToken) .ConfigureAwait(false); + this.UpdateFunctionResultWithOutputs(result); } else diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs index 5791f6e236..aa738a4c0f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Planning; @@ -276,8 +277,8 @@ public sealed class PlanSerializationTests returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input)) .Returns(() => Task.FromResult(new FunctionResult("functionName", "pluginName", returnContext))); - this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((function, variables, ct) => + this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((function, variables, settings, ct) => { var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input); @@ -355,8 +356,8 @@ public sealed class PlanSerializationTests mockFunction.Setup(x => x.Describe()).Returns(new FunctionView("functionName", "pluginName")); - this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((function, variables, ct) => + this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((function, variables, settings, ct) => { var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); c.Variables.TryGetValue("variables", out string? v); @@ -397,7 +398,7 @@ public sealed class PlanSerializationTests // Assert Assert.NotNull(plan); Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString()); - this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); // Act var serializedPlan2 = plan.ToJson(); @@ -458,8 +459,8 @@ public sealed class PlanSerializationTests plan.AddSteps(mockFunction.Object, mockFunction.Object); - this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((function, variables, ct) => + this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((function, variables, settings, ct) => { var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); c.Variables.TryGetValue("variables", out string? v); @@ -498,7 +499,7 @@ public sealed class PlanSerializationTests // Assert Assert.NotNull(plan); Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString()); - this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); // Act var serializedPlan2 = plan.ToJson(); diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs index 28b78b558d..fb0f235c4d 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs @@ -506,8 +506,8 @@ public sealed class PlanTests // Arrange var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (function, variables, ct) => + functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (function, variables, settings, ct) => { var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); @@ -668,8 +668,8 @@ public sealed class PlanTests // Arrange var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (function, variables, ct) => + functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (function, variables, settings, ct) => { var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); @@ -842,8 +842,8 @@ Previously:Outline section #1 of 3: Here is a 3 chapter outline about NovelOutli return new SKContext(functionRunner.Object, serviceProvider.Object, contextVariables, functions); }); - functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (function, variables, ct) => + functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (function, variables, settings, ct) => { var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); From 5bfff5dc4b24039c607b25f5964f64231ac39c37 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:12:29 +0100 Subject: [PATCH 07/24] .Net: Plans using FunctionRunner + RequestSettings (Revert) (#3279) This change actually added a regression on the hooks, duplicating the triggering while using plans. RequestSettings at the Kernel level needd more discussion. Reverts microsoft/semantic-kernel#3264 --- .../Sequential/SequentialPlannerTests.cs | 8 ++--- .../SemanticKernel.Abstractions/IKernel.cs | 5 +--- .../Orchestration/FunctionRunnerExtensions.cs | 29 ------------------- .../Orchestration/IFunctionRunner.cs | 3 -- dotnet/src/SemanticKernel.Core/Kernel.cs | 5 ++-- .../SemanticKernel.Core/KernelExtensions.cs | 27 +++-------------- .../Orchestration/FunctionRunner.cs | 11 ++----- .../src/SemanticKernel.Core/Planning/Plan.cs | 12 +++----- .../Planning/PlanSerializationTests.cs | 17 +++++------ .../Planning/PlanTests.cs | 12 ++++---- 10 files changed, 32 insertions(+), 97 deletions(-) delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs index 75af7b4ae9..200f4088d9 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs @@ -22,8 +22,8 @@ public sealed class SequentialPlannerTests // Arrange var kernel = new Mock(); kernel.Setup(x => x.LoggerFactory).Returns(new Mock().Object); - kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (vars, requestSettings, cancellationToken, functions) => + kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (vars, cancellationToken, functions) => { var functionResult = await functions[0].InvokeAsync(kernel.Object, vars, cancellationToken: cancellationToken); return KernelResult.FromFunctionResults(functionResult.GetValue(), new List { functionResult }); @@ -173,8 +173,8 @@ public sealed class SequentialPlannerTests // Mock Plugins kernel.Setup(x => x.Functions).Returns(functions.Object); - kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (vars, requestSettings, cancellationToken, functions) => + kernel.Setup(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (vars, cancellationToken, functions) => { var functionResult = await functions[0].InvokeAsync(kernel.Object, vars, cancellationToken: cancellationToken); return KernelResult.FromFunctionResults(functionResult.GetValue(), new List { functionResult }); diff --git a/dotnet/src/SemanticKernel.Abstractions/IKernel.cs b/dotnet/src/SemanticKernel.Abstractions/IKernel.cs index c0a17815bf..29b10e9e1f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/IKernel.cs +++ b/dotnet/src/SemanticKernel.Abstractions/IKernel.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Events; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Memory; @@ -53,14 +52,12 @@ public interface IKernel /// Run a pipeline composed of synchronous and asynchronous functions. /// /// Input to process - /// Request settings to use in the functions call /// The to monitor for cancellation requests. The default is . /// List of functions /// Result of the function composition Task RunAsync( ContextVariables variables, - AIRequestSettings? requestSettings = null, - CancellationToken cancellationToken = default, + CancellationToken cancellationToken, params ISKFunction[] pipeline); /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs deleted file mode 100644 index 97294a2fe2..0000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Orchestration/FunctionRunnerExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Orchestration; - -/// -/// Function runner extensions. -/// -public static class FunctionRunnerExtensions -{ - /// - /// Execute a function using the resources loaded in the context. - /// - /// Target function runner - /// Target function to run - /// Input to process - /// The to monitor for cancellation requests. The default is . - /// Result of the function composition - public static Task RunAsync( - this IFunctionRunner functionRunner, - ISKFunction skFunction, - ContextVariables? variables = null, - CancellationToken cancellationToken = default) - { - return functionRunner.RunAsync(skFunction, variables, null, cancellationToken); - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs b/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs index 87227f6dfd..71357d2f30 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Orchestration/IFunctionRunner.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.AI; namespace Microsoft.SemanticKernel.Orchestration; @@ -16,13 +15,11 @@ public interface IFunctionRunner /// /// Target function to run /// Input to process - /// Request settings to override /// The to monitor for cancellation requests. The default is . /// Result of the function composition Task RunAsync( ISKFunction skFunction, ContextVariables? variables = null, - AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default); /// diff --git a/dotnet/src/SemanticKernel.Core/Kernel.cs b/dotnet/src/SemanticKernel.Core/Kernel.cs index 013774b61b..3999733678 100644 --- a/dotnet/src/SemanticKernel.Core/Kernel.cs +++ b/dotnet/src/SemanticKernel.Core/Kernel.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Events; using Microsoft.SemanticKernel.Http; @@ -98,7 +97,7 @@ public sealed class Kernel : IKernel, IDisposable } /// - public async Task RunAsync(ContextVariables variables, AIRequestSettings? requestSettings, CancellationToken cancellationToken = default, params ISKFunction[] pipeline) + public async Task RunAsync(ContextVariables variables, CancellationToken cancellationToken, params ISKFunction[] pipeline) { var context = this.CreateNewContext(variables); @@ -129,7 +128,7 @@ repeat: continue; } - functionResult = await skFunction.InvokeAsync(context, requestSettings, cancellationToken).ConfigureAwait(false); + functionResult = await skFunction.InvokeAsync(context, cancellationToken: cancellationToken).ConfigureAwait(false); context = functionResult.Context; diff --git a/dotnet/src/SemanticKernel.Core/KernelExtensions.cs b/dotnet/src/SemanticKernel.Core/KernelExtensions.cs index 82d57b09d9..41545d15a7 100644 --- a/dotnet/src/SemanticKernel.Core/KernelExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/KernelExtensions.cs @@ -85,26 +85,7 @@ public static class KernelExtensions CancellationToken cancellationToken = default) { Verify.NotNull(kernel); - return kernel.RunAsync(variables ?? new(), null, cancellationToken, skFunction); - } - - /// - /// Run a pipeline composed of synchronous and asynchronous functions. - /// - /// The kernel. - /// Variables to process - /// Cancellation token - /// List of functions - /// Result of the function composition - public static Task RunAsync( - this IKernel kernel, - ContextVariables variables, - CancellationToken cancellationToken = default, - params ISKFunction[] pipeline) - { - Verify.NotNull(kernel); - - return kernel.RunAsync(variables, null, cancellationToken, pipeline); + return kernel.RunAsync(variables ?? new(), cancellationToken, skFunction); } /// @@ -150,7 +131,7 @@ public static class KernelExtensions params ISKFunction[] pipeline) { Verify.NotNull(kernel); - return kernel.RunAsync(variables, null, CancellationToken.None, pipeline); + return kernel.RunAsync(variables, CancellationToken.None, pipeline); } /// @@ -166,7 +147,7 @@ public static class KernelExtensions params ISKFunction[] pipeline) { Verify.NotNull(kernel); - return kernel.RunAsync(new ContextVariables(), null, cancellationToken, pipeline); + return kernel.RunAsync(new ContextVariables(), cancellationToken, pipeline); } /// @@ -184,6 +165,6 @@ public static class KernelExtensions params ISKFunction[] pipeline) { Verify.NotNull(kernel); - return kernel.RunAsync(new ContextVariables(input), null, cancellationToken, pipeline); + return kernel.RunAsync(new ContextVariables(input), cancellationToken, pipeline); } } diff --git a/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs b/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs index ae9dd77888..bb89b166f8 100644 --- a/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs +++ b/dotnet/src/SemanticKernel.Core/Orchestration/FunctionRunner.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.AI; namespace Microsoft.SemanticKernel.Orchestration; @@ -24,13 +23,9 @@ internal class FunctionRunner : IFunctionRunner } /// - public async Task RunAsync( - ISKFunction skFunction, - ContextVariables? variables = null, - AIRequestSettings? requestSettings = null, - CancellationToken cancellationToken = default) + public async Task RunAsync(ISKFunction skFunction, ContextVariables? variables = null, CancellationToken cancellationToken = default) { - return (await this._kernel.RunAsync(variables ?? new(), requestSettings, cancellationToken, skFunction).ConfigureAwait(false)) + return (await this._kernel.RunAsync(skFunction, variables, cancellationToken).ConfigureAwait(false)) .FunctionResults.First(); } @@ -38,6 +33,6 @@ internal class FunctionRunner : IFunctionRunner public Task RunAsync(string pluginName, string functionName, ContextVariables? variables = null, CancellationToken cancellationToken = default) { var function = this._kernel.Functions.GetFunction(pluginName, functionName); - return this.RunAsync(function, variables, null, cancellationToken); + return this.RunAsync(function, variables, cancellationToken); } } diff --git a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs index a702d63679..46c954ad07 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs @@ -246,7 +246,7 @@ public sealed class Plan : IPlan var functionVariables = this.GetNextStepVariables(context.Variables, step); // Execute the step - var result = await context.Runner.RunAsync(step, functionVariables, null, cancellationToken).ConfigureAwait(false); + var result = await context.Runner.RunAsync(step, functionVariables, cancellationToken).ConfigureAwait(false); var resultValue = result.Context.Result.Trim(); @@ -336,14 +336,10 @@ public sealed class Plan : IPlan var functionContext = context.Clone(functionVariables, context.Functions); // Execute the step - - result = await context.Runner.RunAsync( - this.Function.WithInstrumentation(context.LoggerFactory), - functionVariables, - requestSettings, - cancellationToken) + result = await this.Function + .WithInstrumentation(context.LoggerFactory) + .InvokeAsync(functionContext, requestSettings, cancellationToken) .ConfigureAwait(false); - this.UpdateFunctionResultWithOutputs(result); } else diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs index aa738a4c0f..5791f6e236 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Planning; @@ -277,8 +276,8 @@ public sealed class PlanSerializationTests returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input)) .Returns(() => Task.FromResult(new FunctionResult("functionName", "pluginName", returnContext))); - this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((function, variables, settings, ct) => + this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((function, variables, ct) => { var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input); @@ -356,8 +355,8 @@ public sealed class PlanSerializationTests mockFunction.Setup(x => x.Describe()).Returns(new FunctionView("functionName", "pluginName")); - this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((function, variables, settings, ct) => + this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((function, variables, ct) => { var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); c.Variables.TryGetValue("variables", out string? v); @@ -398,7 +397,7 @@ public sealed class PlanSerializationTests // Assert Assert.NotNull(plan); Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString()); - this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); // Act var serializedPlan2 = plan.ToJson(); @@ -459,8 +458,8 @@ public sealed class PlanSerializationTests plan.AddSteps(mockFunction.Object, mockFunction.Object); - this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((function, variables, settings, ct) => + this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((function, variables, ct) => { var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); c.Variables.TryGetValue("variables", out string? v); @@ -499,7 +498,7 @@ public sealed class PlanSerializationTests // Assert Assert.NotNull(plan); Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString()); - this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + this._functionRunner.Verify(x => x.RunAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); // Act var serializedPlan2 = plan.ToJson(); diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs index fb0f235c4d..28b78b558d 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs @@ -506,8 +506,8 @@ public sealed class PlanTests // Arrange var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (function, variables, settings, ct) => + functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (function, variables, ct) => { var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); @@ -668,8 +668,8 @@ public sealed class PlanTests // Arrange var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); - functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (function, variables, settings, ct) => + functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (function, variables, ct) => { var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); @@ -842,8 +842,8 @@ Previously:Outline section #1 of 3: Here is a 3 chapter outline about NovelOutli return new SKContext(functionRunner.Object, serviceProvider.Object, contextVariables, functions); }); - functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(async (function, variables, settings, ct) => + functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (function, variables, ct) => { var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); From 5cad7835b57ce77542607cae401f9b8841e4c849 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:17:57 +0100 Subject: [PATCH 08/24] .Net Removing IPlan interface (#3265) ### Motivation and Context IPlan interface has not definition and all Plan implementations actually implement ISKFunction members. Currently there is no need to have this as a `Flag interface`. ### Description Removing `IPlan` interface and pointing all its dependencies to `ISKFunction` directly. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- dotnet/src/SemanticKernel.Core/Planning/IPlan.cs | 3 +++ .../SemanticKernel.Core/Planning/InstrumentedPlan.cs | 8 ++++---- dotnet/src/SemanticKernel.Core/Planning/Plan.cs | 2 +- .../SemanticKernel.Core/Planning/PlanExtensions.cs | 12 ++++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/dotnet/src/SemanticKernel.Core/Planning/IPlan.cs b/dotnet/src/SemanticKernel.Core/Planning/IPlan.cs index f38b88bd19..e4437ef1e2 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/IPlan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/IPlan.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.SemanticKernel.Planning; /// /// Interface for standard Semantic Kernel callable plan. /// +[Obsolete("This interface is obsoleted, use ISKFunction interface instead")] public interface IPlan : ISKFunction { } diff --git a/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs b/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs index 61e39ca9ee..83c087f6f7 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/InstrumentedPlan.cs @@ -17,7 +17,7 @@ namespace Microsoft.SemanticKernel.Planning; /// /// Standard Semantic Kernel callable plan with instrumentation. /// -internal sealed class InstrumentedPlan : IPlan +internal sealed class InstrumentedPlan : ISKFunction { /// public string Name => this._plan.Name; @@ -31,10 +31,10 @@ internal sealed class InstrumentedPlan : IPlan /// /// Initialize a new instance of the class. /// - /// Instance of to decorate. + /// Instance of to decorate. /// The to use for logging. If null, no logging will be performed. public InstrumentedPlan( - IPlan plan, + ISKFunction plan, ILoggerFactory? loggerFactory = null) { this._plan = plan; @@ -59,7 +59,7 @@ internal sealed class InstrumentedPlan : IPlan #region private ================================================================================ - private readonly IPlan _plan; + private readonly ISKFunction _plan; private readonly ILogger _logger; /// diff --git a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs index 46c954ad07..ba64d6aaa3 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/Plan.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/Plan.cs @@ -22,7 +22,7 @@ namespace Microsoft.SemanticKernel.Planning; /// Plan is used to create trees of s. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public sealed class Plan : IPlan +public sealed class Plan : ISKFunction { /// /// State of the plan diff --git a/dotnet/src/SemanticKernel.Core/Planning/PlanExtensions.cs b/dotnet/src/SemanticKernel.Core/Planning/PlanExtensions.cs index c163c617c4..456cf87a07 100644 --- a/dotnet/src/SemanticKernel.Core/Planning/PlanExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/Planning/PlanExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Linq; using Microsoft.Extensions.Logging; @@ -73,7 +74,18 @@ public static class PlanExtensions /// /// Instance of to decorate. /// The to use for logging. If null, no logging will be performed. + [Obsolete("Use concrete class Plan WithInstrumentation instead")] public static IPlan WithInstrumentation(this IPlan plan, ILoggerFactory? loggerFactory = null) + { + throw new NotSupportedException("This method is obsolete, use concrete class Plan WithInstrumentation instead"); + } + + /// + /// Returns decorated instance of with plan enabled instrumentation. + /// + /// Instance of to decorate. + /// The to use for logging. If null, no logging will be performed. + public static ISKFunction WithInstrumentation(this Plan plan, ILoggerFactory? loggerFactory = null) { return new InstrumentedPlan(plan, loggerFactory); } From ab35598ef4a87beb5a1e57a942d95564802cf213 Mon Sep 17 00:00:00 2001 From: Devis Lucato Date: Tue, 24 Oct 2023 12:19:35 -0700 Subject: [PATCH 09/24] Update README.md (#3287) Update docs about Kernel Memory --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cab9adb1e..ddab6c95ce 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ If you like Semantic Kernel, you may also be interested in other repos the Seman | [Chat Copilot](https://github.com/microsoft/chat-copilot) | A reference application that demonstrates how to build a chatbot with Semantic Kernel. | | [Semantic Kernel Docs](https://github.com/MicrosoftDocs/semantic-kernel-docs) | The home for Semantic Kernel documentation that appears on the Microsoft learn site. | | [Semantic Kernel Starters](https://github.com/microsoft/semantic-kernel-starters) | Starter projects for Semantic Kernel to make it easier to get started. | -| [Semantic Memory](https://github.com/microsoft/semantic-memory) | A service that allows you to create pipelines for ingesting, storing, and querying knowledge. | +| [Kernel Memory](https://github.com/microsoft/kernel-memory) | A scalable Memory service to store information and ask questions using the RAG pattern. | ## Join the community From bcbdadd8a26422ea605597381f691ce3d356c3f6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 25 Oct 2023 17:42:59 +0100 Subject: [PATCH 10/24] ADR for SK prompt syntax for chat completion roles (#3277) ### Motivation and Context ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../0014-chat-completion-roles-in-prompt.md | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs/decisions/0014-chat-completion-roles-in-prompt.md diff --git a/docs/decisions/0014-chat-completion-roles-in-prompt.md b/docs/decisions/0014-chat-completion-roles-in-prompt.md new file mode 100644 index 0000000000..f820865600 --- /dev/null +++ b/docs/decisions/0014-chat-completion-roles-in-prompt.md @@ -0,0 +1,145 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: proposed +date: 2023-10-23 +deciders: markwallace, mabolan +consulted: +informed: +--- +# SK prompt syntax for chat completion roles + +## Context and Problem Statement +Today, SK does not have the ability to mark a block of text in a prompt as a message with a specific role, such as assistant, system, or user. As a result, SK can't chunk the prompt into the list of messages required by chat completion connectors. + +Additionally, prompts can be defined using a range of template syntaxes supported by various template engines, such as Handlebars, Jinja, and others. Each of these syntaxes may represent chat messages or roles in a distinct way. Consequently, the template engine syntax may leak into SK's domain if no proper abstraction is put in place, coupling SK with the template engines and making it impossible to support new ones. + + +## Decision Drivers +* It should be possible to mark a block of text in a prompt as a message with a role so that it can be converted into a list of chat messages for use by chat completion connectors. +* The syntax specific to the template engine message/role should be mapped to the SK message/role syntax to abstract SK from a specific template engine syntax. + +## Considered Options +**1. Message/role tags are generated by functions specified in a prompt.** This option relies on the fact that many template engines can invoke functions specified in the template. Therefore, an internal function can be registered with a template engine, and the function will create a message/model tag based on the provided arguments. The prompt template engine will execute the function and emit the function result into the prompt template, and the rendered prompt will have a section for each message/role decorated with these tags. Here's an example of how this can be done using the SK basic template engine and Handlebars: + + Function: + ```csharp + internal class SystemFunctions + { + public string Message(string role) + { + return $""; + } + } + ``` + + Prompt: + + ```bash + {{message role="system"}} + You are a bank manager. Be helpful, respectful, appreciate diverse language styles. + {{message role="system"}} + + {{message role="user"}} + I want to {{$input}} + {{message role="user"}} + ``` + + Rendered prompt: + + ```xml + + You are a bank manager. Be helpful, respectful, appreciate diverse language styles. + + + I want to buy a house. + + ``` + +**2. Message/role tags are generated by a prompt-specific mechanism.** This option utilizes template engine syntax constructions, helpers, and handlers other than functions to inject SK message/role tags into the final prompt. + In the example below, to parse the prompt that uses the handlebars syntax we need to register a block helper (a callback that is invoked when the Handlebars engine encounters it) to emit the SK message/role tags in the resulting prompt. + + Block helpers: + ```csharp + this.handlebarsEngine.RegisterHelper("system", (EncodedTextWriter output, Context context, Arguments arguments) => { + //Emit the tags + }); + this.handlebarsEngine.RegisterHelper("user", (EncodedTextWriter output, Context context, Arguments arguments) => { + //Emit the tags + }); + ``` + + Prompt: + ```bash + {{#system~}} + You are a bank manager. Be helpful, respectful, appreciate diverse language styles. + {{~/system}} + {{#user~}} + I want to {{$input}} + {{~/user}} + ``` + + Rendered prompt: + ```xml + + You are a bank manager. Be helpful, respectful, appreciate diverse language styles. + + + I want to buy a house. + + ``` + +**3. Message/role tags are applied on top of prompt template engine**. This option presumes specifying the SK message/role tags directly in a prompt to denote message/role blocks in way that template engine does not parse/handle them and considers them as a regular text. + In the example below, the prompt the `` tags are marking boundaries of the system and user messages and SK basic template engine consider them as regular text without processing them. + + Prompt: + ```xml + + You are a bank manager. Be helpful, respectful, appreciate diverse language styles. + + + I want to {{$input}} + + ``` + + Rendered prompt: + ```xml + + You are a bank manager. Be helpful, respectful, appreciate diverse language styles. + + + I want to buy a house. + + ``` + +## Pros and Cons +**1. Message/role tags are generated by functions specified in a prompt** + + Pros: + * Functions can be defined once and reused in prompt templates that support function calling. + + Cons: + * Functions might not be supported by some template engines. + * The system/internal functions should be pre-registered by SK so users don't need to import them. + * Each prompt template engine will have how to discover and call the system/internal functions. + +**2. Message/role tags are generated by prompt specific mechanism** + + Pros: + * Enables message/role representation with the optimal template engine syntax constructions, aligning with other constructions for that specific engine. + + Cons: + * Each prompt template engine will have to register callbacks/handlers to handle template syntax constructions rendering to emit SK message/role tags. + +**3. Message/role tags are applied on top of prompt template engine** + + Pros: + * No changes are required to prompt template engines. + + Cons: + * The message/role tag syntax may not align with other syntax constructions for that template engine. + * Syntax errors in message/role tags will be detected by components parsing the prompt and not by prompt template engines. + +## Decision Outcome +It was agreed not to limit ourselves to only one possible option because it may not be feasible to apply that option to new template engines we might need to support in the future. Instead, each time a new template engine is added, every option should be considered, and the optimal one should be preferred for that particular template engine. + +It was also agreed that, at the moment, we will go with the "3. Message/role tags are applied on top of the prompt template engine" option to support the message/role prompt syntax in SK, which currently uses the `BasicPromptTemplateEngine` engine. \ No newline at end of file From 269846e89666f232a680422307ddee27d6e09e50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:18:44 +0100 Subject: [PATCH 11/24] .Net: Bump Microsoft.Azure.Functions.Worker.Sdk from 1.15.0 to 1.15.1 in /dotnet (#3268) Bumps [Microsoft.Azure.Functions.Worker.Sdk](https://github.com/Azure/azure-functions-dotnet-worker) from 1.15.0 to 1.15.1.
Release notes

Sourced from Microsoft.Azure.Functions.Worker.Sdk's releases.

Microsoft.Azure.Functions.Worker.Sdk 1.15.1

What's Changed

Microsoft.Azure.Functions.Worker.Sdk 1.15.1 (meta package)

  • Include canUsePlaceholder property in worker.config.json (#1956)
Commits
  • a70ead9 Including "canUsePlaceholder" property in worker.config (#1956)
  • da4e1f2 Improved handling of env reload request - handles older version of worker sdk...
  • f730055 Release notes cleanup after sdk-1.15.0 release (#1952)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Microsoft.Azure.Functions.Worker.Sdk&package-manager=nuget&previous-version=1.15.0&new-version=1.15.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/dotnet/KernelHttpServer/KernelHttpServer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/dotnet/KernelHttpServer/KernelHttpServer.csproj b/samples/dotnet/KernelHttpServer/KernelHttpServer.csproj index faed67ed2d..8182bab33e 100644 --- a/samples/dotnet/KernelHttpServer/KernelHttpServer.csproj +++ b/samples/dotnet/KernelHttpServer/KernelHttpServer.csproj @@ -12,7 +12,7 @@ - + From 945fef82a288b692f204c590c96abc2a0ccb2971 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:19:14 +0100 Subject: [PATCH 12/24] .Net: Bump Azure.Identity from 1.10.2 to 1.10.3 in /dotnet (#3269) Bumps [Azure.Identity](https://github.com/Azure/azure-sdk-for-net) from 1.10.2 to 1.10.3.
Release notes

Sourced from Azure.Identity's releases.

Azure.Identity_1.10.3

1.10.3 (2023-10-18)

Bugs Fixed

  • ManagedIdentityCredential will now correctly retry when the instance metadata endpoint returns a 410 response. #28568

Other Changes

  • Updated Microsoft.Identity.Client dependency to version 4.56.0
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Azure.Identity&package-manager=nuget&previous-version=1.10.2&new-version=1.10.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index cfdaca52ce..b8ae82044b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -6,7 +6,7 @@ - + From cf4bfbd57436f0ecce2340b54d88c3939a17f4c3 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:27:13 +0100 Subject: [PATCH 13/24] Update status of all merged ADR's to accepted and add a contact (#3323) ### Motivation and Context Update status of all merged ADR's to accepted and add a contact This will make it clearer to Python and Java squads which decisions are accepted and who to contact for more information ### Description ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- docs/decisions/0001-madr-architecture-decisions.md | 2 +- docs/decisions/0002-java-folder-structure.md | 2 +- docs/decisions/0003-support-multiple-native-function-args.md | 1 + docs/decisions/0004-error-handling.md | 3 ++- docs/decisions/0005-function-execution-handlers.md | 3 ++- .../0006-open-api-dynamic-payload-and-namespaces.md | 4 ++-- docs/decisions/0007-prompt-extract-template-engine.md | 1 + docs/decisions/0008-support-generic-llm-request-settings.md | 3 ++- ...support-multiple-named-args-in-template-function-calls.md | 5 +++-- docs/decisions/0010-dotnet-project-structure.md | 1 + docs/decisions/0010-openai-function-calling.md | 3 ++- docs/decisions/0011-function-and-kernel-result-types.md | 1 + docs/decisions/0012-kernel-service-registration.md | 3 ++- docs/decisions/0013-memory-as-plugin.md | 3 ++- docs/decisions/0014-chat-completion-roles-in-prompt.md | 3 ++- 15 files changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/decisions/0001-madr-architecture-decisions.md b/docs/decisions/0001-madr-architecture-decisions.md index faa92433ed..44008d442b 100644 --- a/docs/decisions/0001-madr-architecture-decisions.md +++ b/docs/decisions/0001-madr-architecture-decisions.md @@ -1,6 +1,6 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted date: 2023-05-29 deciders: dluc,shawncal,hathind,alliscode consulted: diff --git a/docs/decisions/0002-java-folder-structure.md b/docs/decisions/0002-java-folder-structure.md index c02489bd13..9a35f97be4 100644 --- a/docs/decisions/0002-java-folder-structure.md +++ b/docs/decisions/0002-java-folder-structure.md @@ -1,6 +1,6 @@ --- # These are optional elements. Feel free to remove any of them. -status: {proposed} +status: accepted date: 2013-06-19 deciders: shawncal,johnoliver consulted: diff --git a/docs/decisions/0003-support-multiple-native-function-args.md b/docs/decisions/0003-support-multiple-native-function-args.md index 8235b85fb8..276eafb45a 100644 --- a/docs/decisions/0003-support-multiple-native-function-args.md +++ b/docs/decisions/0003-support-multiple-native-function-args.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. status: accepted +contact: markwallace-microsoft date: 2023-06-16 deciders: shawncal,dluc consulted: diff --git a/docs/decisions/0004-error-handling.md b/docs/decisions/0004-error-handling.md index 4f98f03f70..74be52a042 100644 --- a/docs/decisions/0004-error-handling.md +++ b/docs/decisions/0004-error-handling.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: semenshi date: 2023-06-23 deciders: shawncal consulted: stephentoub diff --git a/docs/decisions/0005-function-execution-handlers.md b/docs/decisions/0005-function-execution-handlers.md index b3b4e2939d..ffccf538f5 100644 --- a/docs/decisions/0005-function-execution-handlers.md +++ b/docs/decisions/0005-function-execution-handlers.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: rogerbarreto date: 2023-05-29 deciders: @rogerbarreto, @shawncal, @stephentoub consulted: diff --git a/docs/decisions/0006-open-api-dynamic-payload-and-namespaces.md b/docs/decisions/0006-open-api-dynamic-payload-and-namespaces.md index ffe87013c5..f7faf5f212 100644 --- a/docs/decisions/0006-open-api-dynamic-payload-and-namespaces.md +++ b/docs/decisions/0006-open-api-dynamic-payload-and-namespaces.md @@ -1,6 +1,6 @@ --- -# These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: semenshi date: 2023-08-15 deciders: shawncal consulted: diff --git a/docs/decisions/0007-prompt-extract-template-engine.md b/docs/decisions/0007-prompt-extract-template-engine.md index a72f5adc14..98dd00ccb1 100644 --- a/docs/decisions/0007-prompt-extract-template-engine.md +++ b/docs/decisions/0007-prompt-extract-template-engine.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. status: accepted +contact: markwallace-microsoft date: 2023-08-25 deciders: shawncal consulted: diff --git a/docs/decisions/0008-support-generic-llm-request-settings.md b/docs/decisions/0008-support-generic-llm-request-settings.md index 98f11afff6..43dcafe1f1 100644 --- a/docs/decisions/0008-support-generic-llm-request-settings.md +++ b/docs/decisions/0008-support-generic-llm-request-settings.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: markwallace-microsoft date: 2023-=9-15 deciders: shawncal consulted: stoub, lemiller, dmytrostruk diff --git a/docs/decisions/0009-support-multiple-named-args-in-template-function-calls.md b/docs/decisions/0009-support-multiple-named-args-in-template-function-calls.md index 412d0901a4..40251ece1a 100644 --- a/docs/decisions/0009-support-multiple-named-args-in-template-function-calls.md +++ b/docs/decisions/0009-support-multiple-named-args-in-template-function-calls.md @@ -1,7 +1,8 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed -date: 6/16/2023 +status: accepted +contact: dmytrostruk +date: 2013-06-16 deciders: shawncal, hario90 consulted: dmytrostruk, matthewbolanos informed: lemillermicrosoft diff --git a/docs/decisions/0010-dotnet-project-structure.md b/docs/decisions/0010-dotnet-project-structure.md index 42a929266f..21ff81f6e9 100644 --- a/docs/decisions/0010-dotnet-project-structure.md +++ b/docs/decisions/0010-dotnet-project-structure.md @@ -3,6 +3,7 @@ # These are optional elements. Feel free to remove any of them status: accepted +contact: markwallace-microsoft date: 2023-09-29 deciders: semenshi, dmytrostruk, rbarreto consulted: shawncal, stoub, lemiller diff --git a/docs/decisions/0010-openai-function-calling.md b/docs/decisions/0010-openai-function-calling.md index 06844ca2af..63c605916c 100644 --- a/docs/decisions/0010-openai-function-calling.md +++ b/docs/decisions/0010-openai-function-calling.md @@ -1,5 +1,6 @@ --- -status: proposed +status: accepted +contact: gitri-ms date: 2023-09-21 deciders: gitri-ms, shawncal consulted: lemillermicrosoft, awharrison-28, dmytrostruk, nacharya1 diff --git a/docs/decisions/0011-function-and-kernel-result-types.md b/docs/decisions/0011-function-and-kernel-result-types.md index bf5d655283..90cac4d035 100644 --- a/docs/decisions/0011-function-and-kernel-result-types.md +++ b/docs/decisions/0011-function-and-kernel-result-types.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. status: accepted +contact: dmytrostruk date: 2023-09-21 deciders: shawncal, dmytrostruk consulted: diff --git a/docs/decisions/0012-kernel-service-registration.md b/docs/decisions/0012-kernel-service-registration.md index 050bd13b50..ea7a524e37 100644 --- a/docs/decisions/0012-kernel-service-registration.md +++ b/docs/decisions/0012-kernel-service-registration.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: dmytrostruk date: 2023-10-03 deciders: dmytrostruk consulted: semenshi, rbarreto, markwallace diff --git a/docs/decisions/0013-memory-as-plugin.md b/docs/decisions/0013-memory-as-plugin.md index 99d8e0955c..b255e3243c 100644 --- a/docs/decisions/0013-memory-as-plugin.md +++ b/docs/decisions/0013-memory-as-plugin.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: dmytrostruk date: 2023-09-21 deciders: shawncal, dmytrostruk consulted: diff --git a/docs/decisions/0014-chat-completion-roles-in-prompt.md b/docs/decisions/0014-chat-completion-roles-in-prompt.md index f820865600..046db755ec 100644 --- a/docs/decisions/0014-chat-completion-roles-in-prompt.md +++ b/docs/decisions/0014-chat-completion-roles-in-prompt.md @@ -1,6 +1,7 @@ --- # These are optional elements. Feel free to remove any of them. -status: proposed +status: accepted +contact: semenshi date: 2023-10-23 deciders: markwallace, mabolan consulted: From 840a26bef5ad705d0bd862040cd11b70ae8247d7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:18:02 +0100 Subject: [PATCH 14/24] Completion service type selection ADR (#3305) ### Motivation and Context ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../0015-completion-service-selection.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/decisions/0015-completion-service-selection.md diff --git a/docs/decisions/0015-completion-service-selection.md b/docs/decisions/0015-completion-service-selection.md new file mode 100644 index 0000000000..c669d03798 --- /dev/null +++ b/docs/decisions/0015-completion-service-selection.md @@ -0,0 +1,117 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: accepted +date: 2023-10-25 +deciders: markwallace, mabolan +consulted: +informed: +--- +# Completion service type selection strategy + +## Context and Problem Statement +Today, SK runs all text prompts using the text completion service. With the addition of a new chat completion prompts and potentially other prompt types, such as image, on the horizon, we need a way to select a completion service type to run these prompts. + + +## Decision Drivers +* Semantic function should be able to identify a completion service type to use when processing text, chat, or image prompts. + +## Considered Options +**1. Completion service type identified by the "prompt_type" property.** This option presumes adding the 'prompt_type' property to the prompt template config model class, 'PromptTemplateConfig.' The property will be specified once by a prompt developer and will be used by the 'SemanticFunction' class to decide which completion service type (not instance) to use when resolving an instance of that particular completion service type. + +**Prompt template** +```json +{ + "schema": "1", + "description": "Hello AI, what can you do for me?", + "prompt_type": "", + "models": [...] +} +``` + +**Semantic function pseudocode** +```csharp +if(string.IsNullOrEmpty(promptTemplateConfig.PromptType) || promptTemplateConfig.PromptType == "text") +{ + var service = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); + //render the prompt, call the service, process and return result +} +else (promptTemplateConfig.PromptType == "chat") +{ + var service = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); + //render the prompt, call the service, process and return result +}, +else (promptTemplateConfig.PromptType == "image") +{ + var service = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); + //render the prompt, call the service, process and return result +} +``` + +**Example** + +```json +name: ComicStrip.Create +prompt: "Generate ideas for a comic strip based on {{$input}}. Design characters, develop the plot, ..." +config: { + "schema": 1, + "prompt_type": "text", + ... +} + +name: ComicStrip.Draw +prompt: "Draw the comic strip - {{$comicStrip.Create $input}}" +config: { + "schema": 1, + "prompt_type": "image", + ... +} +``` + +Pros: + - Deterministically specifies which completion service **type** to use, so image prompts won't be rendered by a text completion service, and vice versa. + +Cons: + - Another property to specify by a prompt developer. + + + +**2. Completion service type identified by prompt content.** The idea behind this option is to analyze the rendered prompt by using regex to check for the presence of specific markers associated with the prompt type. For example, the presence of the `` tag in the rendered prompt might indicate that the prompt is a chat prompt and should be handled by the chat completion service. This approach may work reliably when we have two completion service types - text and chat - since the logic would be straightforward: if the message tag is found in the rendered prompt, handle it with the chat completion service; otherwise, use the text completion service. However, this logic becomes unreliable when we start adding new prompt types, and those prompts lack markers specific to their prompt type. For example, if we add an image prompt, we won't be able to distinguish between a text prompt and an image prompt unless the image prompt has a unique marker identifying it as such. + +```csharp +if (Regex.IsMatch(renderedPrompt, @".*?")) +{ + var service = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); + //render the prompt, call the service, process and return result +}, +else +{ + var service = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); + //render the prompt, call the service, process and return result +} +``` + +**Example** + +```json +name: ComicStrip.Create +prompt: "Generate ideas for a comic strip based on {{$input}}. Design characters, develop the plot, ..." +config: { + "schema": 1, + ... +} + +name: ComicStrip.Draw +prompt: "Draw the comic strip - {{$comicStrip.Create $input}}" +config: { + "schema": 1, + ... +} +``` +Pros: +- No need for a new property to identify the prompt type. + +Cons: +- Unreliable unless the prompt contains unique markers specifically identifying the prompt type. + +## Decision Outcome +We decided to choose the '2. Completion service type identified by prompt content' option and will reconsider it when we encounter another completion service type that cannot be supported by this option or when we have a solid set of requirements for using a different mechanism for selecting the completion service type. From d6f7ccc87562a2be7242782a6e0559f918d468a6 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:55:46 +0100 Subject: [PATCH 15/24] ADR Custom Prompt Template Formats (#3286) ### Motivation and Context ADR Custom Prompt Template Formats ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --------- Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../0014-custom-prompt-template-formats.md | 290 ++++++++++++++++++ .../diagrams/prompt-template-factory.mmd | 36 +++ .../diagrams/prompt-template-factory.png | Bin 0 -> 77382 bytes 3 files changed, 326 insertions(+) create mode 100644 docs/decisions/0014-custom-prompt-template-formats.md create mode 100644 docs/decisions/diagrams/prompt-template-factory.mmd create mode 100644 docs/decisions/diagrams/prompt-template-factory.png diff --git a/docs/decisions/0014-custom-prompt-template-formats.md b/docs/decisions/0014-custom-prompt-template-formats.md new file mode 100644 index 0000000000..5a733ce181 --- /dev/null +++ b/docs/decisions/0014-custom-prompt-template-formats.md @@ -0,0 +1,290 @@ +--- +status: approved +contact: markwallace-microsoft +date: 2023-10-26 +deciders: mabolan, markwallace-microsoft, semenshi, rbarreto +consulted: dmytrostruk +informed: +--- +# Custom Prompt Template Formats + +## Context and Problem Statement + +Semantic Kernel currently supports a custom prompt template language that allows for variable interpolation and function execution. +Semantic Kernel allows for custom prompt template formats to be integrated e.g., prompt templates using [Handlebars](https://handlebarsjs.com/) syntax. + +The purpose of this ADR is to describe how a custom prompt template formats will be supported in the Semantic Kernel. + +### Current Design + +By default the `Kernel` uses the `BasicPromptTemplateEngine` which supports the Semantic Kernel specific template format. + +#### Code Patterns + +Below is an expanded example of how to create a semantic function from a prompt template string which uses the built-in Semantic Kernel format: + +```csharp +IKernel kernel = Kernel.Builder + .WithPromptTemplateEngine(new BasicPromptTemplateEngine()) + .WithOpenAIChatCompletionService( + modelId: openAIModelId, + apiKey: openAIApiKey) + .Build(); + +kernel.ImportFunctions(new TimePlugin(), "time"); + +string templateString = "Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?"; +var promptTemplateConfig = new PromptTemplateConfig(); +var promptTemplate = new PromptTemplate(templateString, promptTemplateConfig, kernel.PromptTemplateEngine); +var kindOfDay = kernel.RegisterSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate); + +var result = await kernel.RunAsync(kindOfDay); +Console.WriteLine(result.GetValue()); +``` + +We have an extension method `var kindOfDay = kernel.CreateSemanticFunction(promptTemplate);` to simplify the process to create and register a semantic function but the expanded format is shown above to highlight the dependency on `kernel.PromptTemplateEngine`. +Also the `BasicPromptTemplateEngine` is the default prompt template engine and will be loaded automatically if the package is available and not other prompt template engine is specified. + +Some issues with this: + +1. You need to have a `Kernel` instance to create a semantic function, which is contrary to one of the goals of allow semantic functions to be created once and reused across multiple `Kernel` instances. +1. `Kernel` only supports a single `IPromptTemplateEngine` so we cannot support using multiple prompt templates at the same time. +1. `IPromptTemplateEngine` is stateless and must perform a parse of the template for each render +1. Our semantic function extension methods relay on our implementation of `IPromptTemplate` (i.e., `PromptTemplate`) which stores the template string and uses the `IPromptTemplateEngine` to render it every time. Note implementations of `IPromptTemplate` are currently stateful as they also store the parameters. + +#### Performance + +The `BasicPromptTemplateEngine` uses the `TemplateTokenizer` to parse the template i.e. extract the blocks. +Then it renders the template i.e. inserts variables and executes functions. Some sample timings for these operations: + +| Operation | Ticks | Milliseconds | +|------------------|---------|--------------| +| Extract blocks | 1044427 | 103 | +| Render variables | 168 | 0 | + +Sample template used was: `"{{variable1}} {{variable2}} {{variable3}} {{variable4}} {{variable5}}"` + +**Note: We will use the sample implementation to support the f-string template format.** + +Using `HandlebarsDotNet` for the same use case results in the following timings: + +| Operation | Ticks | Milliseconds | +|------------------|---------|--------------| +| Compile template | 66277 | 6 | +| Render variables | 4173 | 0 | + +**By separating the extract blocks/compile from the render variables operation it will be possible to optimise performance by compiling templates just once.** + +#### Implementing a Custom Prompt Template Engine + +There are two interfaces provided: + +```csharp +public interface IPromptTemplateEngine +{ + Task RenderAsync(string templateText, SKContext context, CancellationToken cancellationToken = default); +} + +public interface IPromptTemplate +{ + IReadOnlyList Parameters { get; } + + public Task RenderAsync(SKContext executionContext, CancellationToken cancellationToken = default); +} +``` + +A prototype implementation of a handlebars prompt template engine could look something like this: + +```csharp +public class HandlebarsTemplateEngine : IPromptTemplateEngine +{ + private readonly ILoggerFactory _loggerFactory; + + public HandlebarsTemplateEngine(ILoggerFactory? loggerFactory = null) + { + this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + } + + public async Task RenderAsync(string templateText, SKContext context, CancellationToken cancellationToken = default) + { + var handlebars = HandlebarsDotNet.Handlebars.Create(); + + var functionViews = context.Functions.GetFunctionViews(); + foreach (FunctionView functionView in functionViews) + { + var skfunction = context.Functions.GetFunction(functionView.PluginName, functionView.Name); + handlebars.RegisterHelper($"{functionView.PluginName}_{functionView.Name}", async (writer, hcontext, parameters) => + { + var result = await skfunction.InvokeAsync(context).ConfigureAwait(true); + writer.WriteSafeString(result.GetValue()); + }); + } + + var template = handlebars.Compile(templateText); + + var prompt = template(context.Variables); + + return await Task.FromResult(prompt).ConfigureAwait(true); + } +} +``` + +**Note: This is just a prototype implementation for illustration purposes only.** + +Some issues: + +1. The `IPromptTemplate` interface is not used and causes confusion. +1. There is no way to allow developers to support multiple prompt template formats at the same time. + + +There is one implementation of `IPromptTemplate` provided in the Semantic Kernel core package. +The `RenderAsync` implementation just delegates to the `IPromptTemplateEngine`. +The `Parameters` list get's populated with the parameters defined in the `PromptTemplateConfig` and any missing variables defined in the template. + +#### Handlebars Considerations + +Handlebars does not support dynamic binding of helpers. Consider the following snippet: + +```csharp +HandlebarsHelper link_to = (writer, context, parameters) => +{ + writer.WriteSafeString($"{context["text"]}"); +}; + +string source = @"Click here: {{link_to}}"; + +var data = new +{ + url = "https://github.com/rexm/handlebars.net", + text = "Handlebars.Net" +}; + +// Act +var handlebars = HandlebarsDotNet.Handlebars.Create(); +handlebars.RegisterHelper("link_to", link_to); +var template = handlebars1.Compile(source); +// handlebars.RegisterHelper("link_to", link_to); This also works +var result = template1(data); +``` + +Handlebars allows the helpers to be registered with the `Handlebars` instance either before or after a template is compiled. +The optimum would be to have a shared `Handlebars` instance for a specific collection of functions and register the helpers just once. +For use cases where the Kernel function collection may have been mutated we will be forced to create a `Handlebars` instance at render time +and then register the helpers. This means we cannot take advantage of the performance improvement provided by compiling the template. + +## Decision Drivers + +In no particular order: + +* Support creating a semantic function without a `IKernel`instance. +* Support late binding of functions i.e., having functions resolved when the prompt is rendered. +* Support allowing the prompt template to be parsed (compiled) just once to optimize performance if needed. +* Support using multiple prompt template formats with a single `Kernel` instance. +* Provide simple abstractions which allow third parties to implement support for custom prompt template formats. + +## Considered Options + +* Obsolete `IPromptTemplateEngine` and replace with `IPromptTemplateFactory`. +* + +### Obsolete `IPromptTemplateEngine` and replace with `IPromptTemplateFactory` + +ISKFunction class relationships + +Below is an expanded example of how to create a semantic function from a prompt template string which uses the built-in Semantic Kernel format: + +```csharp +// Semantic function can be created once +var promptTemplateFactory = new BasicPromptTemplateFactory(); +string templateString = "Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?"; +var promptTemplateConfig = new PromptTemplateConfig(); +// Line below will replace the commented out code +var promptTemplate = promptTemplateFactory.CreatePromptTemplate(templateString, promptTemplateConfig); +var kindOfDay = ISKFunction.CreateSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate) +// var promptTemplate = new PromptTemplate(promptTemplate, promptTemplateConfig, kernel.PromptTemplateEngine); +// var kindOfDay = kernel.RegisterSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate); + +// Create Kernel after creating the semantic function +// Later we will support passing a function collection to the KernelBuilder +IKernel kernel = Kernel.Builder + .WithOpenAIChatCompletionService( + modelId: openAIModelId, + apiKey: openAIApiKey) + .Build(); + +kernel.ImportFunctions(new TimePlugin(), "time"); +// Optionally register the semantic function with the Kernel +kernel.RegisterCustomFunction(kindOfDay); + +var result = await kernel.RunAsync(kindOfDay); +Console.WriteLine(result.GetValue()); +``` + +**Notes:** + +* `BasicPromptTemplateFactory` will be the default implementation and will be automatically provided in `KernelSemanticFunctionExtensions`. Developers will also be able to provide their own implementation. +* The factory uses the new `PromptTemplateConfig.TemplateFormat` to create the appropriate `IPromptTemplate` instance. +* We should look to remove `promptTemplateConfig` as a parameter to `CreateSemanticFunction`. That change is outside of the scope of this ADR. + +The `BasicPromptTemplateFactory` and `BasicPromptTemplate` implementations look as follows: + +```csharp +public sealed class BasicPromptTemplateFactory : IPromptTemplateFactory +{ + private readonly IPromptTemplateFactory _promptTemplateFactory; + private readonly ILoggerFactory _loggerFactory; + + public BasicPromptTemplateFactory(IPromptTemplateFactory promptTemplateFactory, ILoggerFactory? loggerFactory = null) + { + this._promptTemplateFactory = promptTemplateFactory; + this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + } + + public IPromptTemplate? CreatePromptTemplate(string templateString, PromptTemplateConfig promptTemplateConfig) + { + if (promptTemplateConfig.TemplateFormat.Equals(PromptTemplateConfig.SEMANTICKERNEL, System.StringComparison.Ordinal)) + { + return new BasicPromptTemplate(templateString, promptTemplateConfig, this._loggerFactory); + } + else if (this._promptTemplateFactory is not null) + { + return this._promptTemplateFactory.CreatePromptTemplate(templateString, promptTemplateConfig); + } + + throw new SKException($"Invalid prompt template format {promptTemplateConfig.TemplateFormat}"); + } +} + +public sealed class BasicPromptTemplate : IPromptTemplate +{ + public BasicPromptTemplate(string templateString, PromptTemplateConfig promptTemplateConfig, ILoggerFactory? loggerFactory = null) + { + this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + this._logger = this._loggerFactory.CreateLogger(typeof(BasicPromptTemplate)); + this._templateString = templateString; + this._promptTemplateConfig = promptTemplateConfig; + this._parameters = new(() => this.InitParameters()); + this._blocks = new(() => this.ExtractBlocks(this._templateString)); + this._tokenizer = new TemplateTokenizer(this._loggerFactory); + } + + public IReadOnlyList Parameters => this._parameters.Value; + + public async Task RenderAsync(SKContext executionContext, CancellationToken cancellationToken = default) + { + return await this.RenderAsync(this._blocks.Value, executionContext, cancellationToken).ConfigureAwait(false); + } + + // Not showing the implementation details +} +``` + +**Note:** + +* The call to `ExtractBlocks` is called lazily once for each prompt template +* The `RenderAsync` doesn't need to extract the blocks every time + +## Decision Outcome + +Chosen option: "Obsolete `IPromptTemplateEngine` and replace with `IPromptTemplateFactory`", because +addresses the requirements and provides good flexibility for the future. diff --git a/docs/decisions/diagrams/prompt-template-factory.mmd b/docs/decisions/diagrams/prompt-template-factory.mmd new file mode 100644 index 0000000000..7c53cb4e5d --- /dev/null +++ b/docs/decisions/diagrams/prompt-template-factory.mmd @@ -0,0 +1,36 @@ +--- +title: IPromptTemplateFactory +--- +classDiagram + %% Use https://mermaid.live/ to preview this diagram. The VS Code extension does not handle namespaces. + direction BT + namespace SemanticKernel { + class IPromptTemplate { + <> + IReadOnlyList Parameters; + public Task RenderAsync(SKContext executionContext, CancellationToken cancellationToken = default); + } + + class IPromptTemplateFactory { + <> + IPromptTemplate CreatePromptTemplate(string template, PromptTemplateConfig config) + } + } + + namespace TemplateEngine_Basic { + class BasicPromptTemplate + class FStringPromptTemplate + class BasicPromptTemplateFactory + } + + namespace TemplateEngine_Handlebars { + class HandlebarsPromptTemplate + class HandlebarsPromptTemplateFactory + } + + IPromptTemplateFactory <|.. BasicPromptTemplateFactory + IPromptTemplateFactory <|.. HandlebarsPromptTemplateFactory + + IPromptTemplate <|.. BasicPromptTemplate + IPromptTemplate <|.. FStringPromptTemplate + IPromptTemplate <|.. HandlebarsPromptTemplate diff --git a/docs/decisions/diagrams/prompt-template-factory.png b/docs/decisions/diagrams/prompt-template-factory.png new file mode 100644 index 0000000000000000000000000000000000000000..19c99157716a421e54bbc8b3a07e0d7476a4e944 GIT binary patch literal 77382 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%0Di< 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 Date: Fri, 27 Oct 2023 19:04:11 +0100 Subject: [PATCH 16/24] .Net: ADR with proposal for the multiple LLM support use cases (#3040) ### Motivation and Context - ADR describing the design for multiple LLM support. This will be expanded in the future to allow additional use cases such as selecting an AI service by LLM model id. - The `IAIServiceSelector` has changed so that it is added to the `IKernel` instance, see example 62 for an example of a custom `IAIServiceSelector` implementation. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- ...emantic-function-multiple-model-support.md | 172 ++++++++++++++++++ .../Example61_MultipleLLMs.cs | 69 +++++++ .../Example62_CustomAIServiceSelector.cs | 164 +++++++++++++++++ .../Prompt/Blocks/CodeBlockTests.cs | 23 +-- .../Prompt/PromptTemplateEngineTests.cs | 7 +- .../Action/ActionPlannerTests.cs | 5 +- ...adOnlyFunctionCollectionExtensionsTests.cs | 12 +- .../Sequential/SequentialPlanParserTests.cs | 11 +- .../Sequential/SequentialPlannerTests.cs | 8 +- .../Orchestration/SKContext.cs | 11 +- .../IAIServiceSelector.cs | 3 +- .../KernelSemanticFunctionExtensions.cs | 12 +- .../Functions/DelegatingAIServiceSelector.cs | 2 +- .../Functions/OrderedIAIServiceSelector.cs | 2 +- .../Functions/SemanticFunction.cs | 20 +- dotnet/src/SemanticKernel.Core/Kernel.cs | 11 +- .../src/SemanticKernel.Core/KernelBuilder.cs | 13 +- .../TemplateEngine/PromptTemplateConfig.cs | 6 +- ...redIAIServiceConfigurationProviderTests.cs | 30 ++- .../Functions/SKContextTests.cs | 13 +- .../Functions/SKFunctionTests2.cs | 2 + .../Orchestration/FunctionResultTests.cs | 4 +- .../Orchestration/KernelResultTests.cs | 4 +- .../Planning/PlanSerializationTests.cs | 35 ++-- .../Planning/PlanTests.cs | 86 ++++----- 25 files changed, 590 insertions(+), 135 deletions(-) create mode 100644 docs/decisions/0015-semantic-function-multiple-model-support.md create mode 100644 dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs create mode 100644 dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs rename dotnet/src/SemanticKernel.Abstractions/{Functions => Services}/IAIServiceSelector.cs (82%) diff --git a/docs/decisions/0015-semantic-function-multiple-model-support.md b/docs/decisions/0015-semantic-function-multiple-model-support.md new file mode 100644 index 0000000000..318285639f --- /dev/null +++ b/docs/decisions/0015-semantic-function-multiple-model-support.md @@ -0,0 +1,172 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: approved +contact: markwallace-microsoft +date: 2023-10-26 +deciders: markwallace-microsoft, semenshi, rogerbarreto +consulted: mabolan, dmytrostruk +informed: +--- +# Multiple Model Support for Semantic Functions + +## Context and Problem Statement + +Developers need to be able to use multiple models e.g., using chat completion together with embeddings. + +## Use Cases + +In scope for Semantic Kernel V1.0 + +* Select Model Request Settings by Service Id. + * A Service Id uniquely identifies a registered AI Service and is typically defined in the scope of an application. +* Select AI Service and Model Request Settings By Developer Defined Strategy. + * A Developer Defined Strategy is a code first approach where a developer provides the logic. + +Out of scope for V1.0 + +* Select Model Request Settings by Model Id. + * A Model Id uniquely identifies a Large Language Model. Multiple AI service providers can support the same LLM. +* Select Model Request Settings by Provider Id and Model Id + * A Provider Id uniquely identifies an AI provider e.g. "Azure OpenAI", "OpenAI", "Hugging Face" + +## Decision Outcome + +Support just use cases listed in this ADR. + +## Descriptions of the Use Cases + +**Note: All code is pseudo code and does not accurately reflect what the final implementations will look like.** + +### Select Model Request Settings by Service Id + +_As a developer using the Semantic Kernel I can configure multiple request settings for a semantic function and associate each one with a service id so that the correct request settings are used when different services are used to execute my semantic function._ + +The semantic function template configuration allows multiple model request settings to be configured. In this case the developer configures different settings based on the service id that is used to execute the semantic function. +In the example below the semantic function is executed with "AzureText" using `max_tokens=60` because "AzureText" is the first service id in the list of models configured for the prompt. + +```csharp +// Configure a Kernel with multiple LLM's +IKernel kernel = Kernel.Builder + .WithLoggerFactory(ConsoleLogger.LoggerFactory) + .WithAzureTextCompletionService(deploymentName: aoai.DeploymentName, + endpoint: aoai.Endpoint, serviceId: "AzureText", apiKey: aoai.ApiKey) + .WithAzureChatCompletionService(deploymentName: aoai.ChatDeploymentName, + endpoint: aoai.Endpoint, serviceId: "AzureChat", apiKey: aoai.ApiKey) + .WithOpenAITextCompletionService(modelId: oai.ModelId, + serviceId: "OpenAIText", apiKey: oai.ApiKey) + .WithOpenAIChatCompletionService(modelId: oai.ChatModelId, + serviceId: "OpenAIChat", apiKey: oai.ApiKey, setAsDefault: true) + .Build(); + +// Configure semantic function with multiple LLM request settings +string configPayload = @"{ + ""schema"": 1, + ""description"": ""Hello AI, what can you do for me?"", + ""models"": [ + { ""service_id"": ""AzureText"", ""max_tokens"": 60 }, + { ""service_id"": ""AzureChat"", ""max_tokens"": 120 }, + { ""service_id"": ""OpenAIText"", ""max_tokens"": 180 }, + { ""service_id"": ""OpenAIChat"", ""max_tokens"": 240 } + ] +}"; +var templateConfig = JsonSerializer.Deserialize(configPayload); +var func = kernel.CreateSemanticFunction(prompt, config: templateConfig!, "HelloAI"); + +// Semantic function is executed with OpenAIChat using max_tokens=240 +result = await kernel.RunAsync(func); +``` + +This works by using the `IAIServiceSelector` interface as the strategy for selecting the AI service and request settings to user when invoking a semantic function. +The interface is defined as follows: + +```csharp +public interface IAIServiceSelector +{ + (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService; +} +``` + +A default `OrderedIAIServiceSelector` implementation is used which selects the AI service based on the order of the model request settings defined for the semantic function. +The implementation checks if a service exists which the corresponding service id and if it does it and the associated model request settings will be used. +In no model request settings are defined then the default text completion service is used. +A default set of request settings can be specified by leaving the service id undefined or empty, the first such default will be used. +If no default if specified and none of the specified services are available the operation will fail. + +### Select AI Service and Model Request Settings By Developer Defined Strategy + +_As a developer using the Semantic Kernel I can provide factories which select the AI service and request settings used to execute my function so that I can dynamically control which AI service is used to execute my semantic function._ + +In this case the developer configures different settings based on the model id and provides an AI Service factory which determines which AI Service will be used when the semantic function is executed. +In the example below the semantic function is executed with whatever AI Service `myServiceFactory` returns. + +```csharp +// Configure a Kernel with multiple LLM's +IKernel kernel = Kernel.Builder + .WithLoggerFactory(ConsoleLogger.LoggerFactory) + .WithAzureTextCompletionService(deploymentName: aoai.DeploymentName, // text-davinci-003 + endpoint: aoai.Endpoint, serviceId: "AzureText", apiKey: aoai.ApiKey) + .WithAzureChatCompletionService(deploymentName: aoai.ChatDeploymentName, // gpt-35-turbo + endpoint: aoai.Endpoint, serviceId: "AzureChat", apiKey: aoai.ApiKey) + .WithOpenAITextCompletionService(modelId: oai.ModelId, // text-davinci-003 + serviceId: "OpenAIText", apiKey: oai.ApiKey, setAsDefault: true) + .WithOpenAIChatCompletionService(modelId: oai.ChatModelId, // gpt-3.5-turbo + serviceId: "OpenAIChat", apiKey: oai.ApiKey) + .Build(); + +// Configure semantic function with multiple LLM request settings +string configPayload = @"{ + ""schema"": 1, + ""description"": ""Hello AI, what can you do for me?"", + ""models"": [ + { ""max_tokens"": 60 }, + { ""max_tokens"": 120 }, + { ""max_tokens"": 180 } + ] +}"; +var templateConfig = JsonSerializer.Deserialize(configPayload); +var func = kernel.CreateSemanticFunction(prompt, config: templateConfig!, "HelloAI"); + +// Semantic function is executed with AI Service and AI request Settings dynamically determined +result = await kernel.RunAsync(func, funcVariables); +``` + +## More Information + +### Select AI Service by Service Id + +The following use case is supported. Developers can create a `Kernel`` instance with multiple named AI services. When invoking a semantic function the service id (and optionally request settings to be used) can be specified. The named AI service will be used to execute the prompt. + +```csharp +var aoai = TestConfiguration.AzureOpenAI; +var oai = TestConfiguration.OpenAI; + +// Configure a Kernel with multiple LLM's +IKernel kernel = Kernel.Builder + .WithLoggerFactory(ConsoleLogger.LoggerFactory) + .WithAzureTextCompletionService(deploymentName: aoai.DeploymentName, + endpoint: aoai.Endpoint, serviceId: "AzureText", apiKey: aoai.ApiKey) + .WithAzureChatCompletionService(deploymentName: aoai.ChatDeploymentName, + endpoint: aoai.Endpoint, serviceId: "AzureChat", apiKey: aoai.ApiKey) + .WithOpenAITextCompletionService(modelId: oai.ModelId, + serviceId: "OpenAIText", apiKey: oai.ApiKey) + .WithOpenAIChatCompletionService(modelId: oai.ChatModelId, + serviceId: "OpenAIChat", apiKey: oai.ApiKey) + .Build(); + +// Invoke the semantic function and service and request settings to use +result = await kernel.InvokeSemanticFunctionAsync(prompt, + requestSettings: new OpenAIRequestSettings() + { ServiceId = "AzureText", MaxTokens = 60 }); + +result = await kernel.InvokeSemanticFunctionAsync(prompt, + requestSettings: new OpenAIRequestSettings() + { ServiceId = "AzureChat", MaxTokens = 120 }); + +result = await kernel.InvokeSemanticFunctionAsync(prompt, + requestSettings: new OpenAIRequestSettings() + { ServiceId = "OpenAIText", MaxTokens = 180 }); + +result = await kernel.InvokeSemanticFunctionAsync(prompt, + requestSettings: new OpenAIRequestSettings() + { ServiceId = "OpenAIChat", MaxTokens = 240 }); +``` diff --git a/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs b/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs new file mode 100644 index 0000000000..66de8ae939 --- /dev/null +++ b/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI; +using RepoUtils; + +// ReSharper disable once InconsistentNaming +public static class Example61_MultipleLLMs +{ + /// + /// Show how to run a semantic function and specify a specific service to use. + /// + public static async Task RunAsync() + { + Console.WriteLine("======== Example61_MultipleLLMs ========"); + + string apiKey = TestConfiguration.AzureOpenAI.ApiKey; + string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; + string endpoint = TestConfiguration.AzureOpenAI.Endpoint; + + if (apiKey == null || chatDeploymentName == null || endpoint == null) + { + Console.WriteLine("Azure endpoint, apiKey, or deploymentName not found. Skipping example."); + return; + } + + string openAIModelId = TestConfiguration.OpenAI.ChatModelId; + string openAIApiKey = TestConfiguration.OpenAI.ApiKey; + + if (openAIModelId == null || openAIApiKey == null) + { + Console.WriteLine("OpenAI credentials not found. Skipping example."); + return; + } + + IKernel kernel = new KernelBuilder() + .WithLoggerFactory(ConsoleLogger.LoggerFactory) + .WithAzureChatCompletionService( + deploymentName: chatDeploymentName, + endpoint: endpoint, + serviceId: "AzureOpenAIChat", + apiKey: apiKey) + .WithOpenAIChatCompletionService( + modelId: openAIModelId, + serviceId: "OpenAIChat", + apiKey: openAIApiKey) + .Build(); + + await RunSemanticFunctionAsync(kernel, "AzureOpenAIChat"); + await RunSemanticFunctionAsync(kernel, "OpenAIChat"); + } + + public static async Task RunSemanticFunctionAsync(IKernel kernel, string serviceId) + { + Console.WriteLine($"======== {serviceId} ========"); + + var prompt = "Hello AI, what can you do for me?"; + + var result = await kernel.InvokeSemanticFunctionAsync( + prompt, + requestSettings: new AIRequestSettings() + { + ServiceId = serviceId + }); + Console.WriteLine(result.GetValue()); + } +} diff --git a/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs b/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs new file mode 100644 index 0000000000..bd33907885 --- /dev/null +++ b/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.ML.Tokenizers; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Connectors.AI.OpenAI; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TemplateEngine; +using RepoUtils; + +// ReSharper disable once InconsistentNaming +public static class Example62_CustomAIServiceSelector +{ + /// + /// Show how to configure model request settings + /// + public static async Task RunAsync() + { + Console.WriteLine("======== Example61_CustomAIServiceSelector ========"); + + string apiKey = TestConfiguration.AzureOpenAI.ApiKey; + string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; + string endpoint = TestConfiguration.AzureOpenAI.Endpoint; + + if (apiKey == null || chatDeploymentName == null || endpoint == null) + { + Console.WriteLine("Azure endpoint, apiKey, or deploymentName not found. Skipping example."); + return; + } + + string openAIModelId = TestConfiguration.OpenAI.ChatModelId; + string openAIApiKey = TestConfiguration.OpenAI.ApiKey; + + if (openAIModelId == null || openAIApiKey == null) + { + Console.WriteLine("OpenAI credentials not found. Skipping example."); + return; + } + + IKernel kernel = new KernelBuilder() + .WithLoggerFactory(ConsoleLogger.LoggerFactory) + .WithAzureChatCompletionService( + deploymentName: chatDeploymentName, + endpoint: endpoint, + serviceId: "AzureOpenAIChat", + apiKey: apiKey) + .WithOpenAIChatCompletionService( + modelId: openAIModelId, + serviceId: "OpenAIChat", + apiKey: openAIApiKey) + .WithAIServiceSelector(new MyAIServiceSelector()) + .Build(); + + var modelSettings = new List + { + new OpenAIRequestSettings() { ServiceId = "AzureOpenAIChat", MaxTokens = 400 }, + new OpenAIRequestSettings() { ServiceId = "OpenAIChat", MaxTokens = 200 } + }; + + await RunSemanticFunctionAsync(kernel, "Hello AI, what can you do for me?", modelSettings); + await RunSemanticFunctionAsync(kernel, "Hello AI, provide an indepth description of what can you do for me as a bulleted list?", modelSettings); + } + + public static async Task RunSemanticFunctionAsync(IKernel kernel, string prompt, List modelSettings) + { + Console.WriteLine($"======== {prompt} ========"); + + var promptTemplateConfig = new PromptTemplateConfig() { ModelSettings = modelSettings }; + var promptTemplate = new PromptTemplate(prompt, promptTemplateConfig, kernel); + + var skfunction = kernel.RegisterSemanticFunction( + "MyFunction", + promptTemplateConfig, + promptTemplate); + + var result = await kernel.RunAsync(skfunction); + Console.WriteLine(result.GetValue()); + } +} + +public class MyAIServiceSelector : IAIServiceSelector +{ + private readonly int _defaultMaxTokens = 300; + private readonly int _minResponseTokens = 150; + + public (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + { + if (modelSettings is null || modelSettings.Count == 0) + { + var service = serviceProvider.GetService(null); + if (service is not null) + { + return (service, null); + } + } + else + { + var tokens = this.CountTokens(renderedPrompt); + + string? serviceId = null; + int fewestTokens = 0; + AIRequestSettings? requestSettings = null; + AIRequestSettings? defaultRequestSettings = null; + foreach (var model in modelSettings) + { + if (!string.IsNullOrEmpty(model.ServiceId)) + { + if (model is OpenAIRequestSettings openAIModel) + { + var responseTokens = (openAIModel.MaxTokens ?? this._defaultMaxTokens) - tokens; + if (serviceId is null || (responseTokens > this._minResponseTokens && responseTokens < fewestTokens)) + { + fewestTokens = responseTokens; + serviceId = model.ServiceId; + requestSettings = model; + } + } + } + else + { + // First request settings with empty or null service id is the default + defaultRequestSettings ??= model; + } + } + Console.WriteLine($"Prompt tokens: {tokens}, Response tokens: {fewestTokens}"); + + if (serviceId is not null) + { + Console.WriteLine($"Selected service: {serviceId}"); + var service = serviceProvider.GetService(serviceId); + if (service is not null) + { + return (service, requestSettings); + } + } + + if (defaultRequestSettings is not null) + { + var service = serviceProvider.GetService(null); + if (service is not null) + { + return (service, defaultRequestSettings); + } + } + } + + throw new SKException("Unable to find AI service to handled request."); + } + + /// + /// MicrosoftML token counter implementation. + /// + private int CountTokens(string input) + { + Tokenizer tokenizer = new(new Bpe()); + var tokens = tokenizer.Encode(input).Tokens; + + return tokens.Count; + } +} diff --git a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs index 8004cedc61..3a26b1bf94 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/Blocks/CodeBlockTests.cs @@ -23,6 +23,7 @@ public class CodeBlockTests private readonly ILoggerFactory _logger = NullLoggerFactory.Instance; private readonly Mock _functionRunner = new(); private readonly Mock _serviceProvider = new(); + private readonly Mock _serviceSelector = new(); public CodeBlockTests() { @@ -34,7 +35,7 @@ public class CodeBlockTests { // Arrange var functionRunner = new Mock(); - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object); var target = new CodeBlock("functionName", this._logger); this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -51,7 +52,7 @@ public class CodeBlockTests public async Task ItThrowsIfAFunctionCallThrowsAsync() { // Arrange - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, functions: this._functions.Object); var function = new Mock(); function .Setup(x => x.InvokeAsync(It.IsAny(), It.IsAny(), It.IsAny())) @@ -147,7 +148,7 @@ public class CodeBlockTests { // Arrange var variables = new ContextVariables { ["varName"] = "foo" }; - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables, functions: this._functions.Object); // Act var codeBlock = new CodeBlock("$varName", NullLoggerFactory.Instance); @@ -162,7 +163,7 @@ public class CodeBlockTests { // Arrange var variables = new ContextVariables { ["varName"] = "bar" }; - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables, functions: this._functions.Object); var varBlock = new VarBlock("$varName"); // Act @@ -177,7 +178,7 @@ public class CodeBlockTests public async Task ItRendersCodeBlockConsistingOfJustAValBlock1Async() { // Arrange - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object); // Act var codeBlock = new CodeBlock("'ciao'", NullLoggerFactory.Instance); @@ -192,7 +193,7 @@ public class CodeBlockTests { // Arrange var kernel = new Mock(); - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object); var valBlock = new ValBlock("'arrivederci'"); // Act @@ -211,7 +212,7 @@ public class CodeBlockTests const string Plugin = "pluginName"; var variables = new ContextVariables { ["input"] = "zero", ["var1"] = "uno", ["var2"] = "due" }; - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var canary0 = string.Empty; @@ -259,7 +260,7 @@ public class CodeBlockTests const string VarValue = "varValue"; var variables = new ContextVariables { [Var] = VarValue }; - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var varBlock = new VarBlock($"${Var}"); @@ -292,7 +293,7 @@ public class CodeBlockTests const string Plugin = "pluginName"; const string Value = "value"; - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables: null, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables: null, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var valBlock = new ValBlock($"'{Value}'"); @@ -330,7 +331,7 @@ public class CodeBlockTests var variables = new ContextVariables(); variables.Set("bob", BobValue); variables.Set("input", Value); - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables: variables, functions: this._functions.Object); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables: variables, functions: this._functions.Object); var funcId = new FunctionIdBlock(Func); var namedArgBlock1 = new NamedArgBlock($"foo='{FooValue}'"); var namedArgBlock2 = new NamedArgBlock("baz=$bob"); @@ -364,7 +365,7 @@ public class CodeBlockTests this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((pluginName, functionName, variables, cancellationToken) => { - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables); return function.InvokeAsync(context, null, cancellationToken); }); } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs index 83a6be8cd7..c394a54d0d 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/TemplateEngine/Prompt/PromptTemplateEngineTests.cs @@ -31,6 +31,7 @@ public sealed class PromptTemplateEngineTests private readonly Mock _kernel; private readonly Mock _functionRunner; private readonly Mock _serviceProvider; + private readonly Mock _serviceSelector; public PromptTemplateEngineTests(ITestOutputHelper testOutputHelper) { @@ -41,6 +42,7 @@ public sealed class PromptTemplateEngineTests this._kernel = new Mock(); this._functionRunner = new Mock(); this._serviceProvider = new Mock(); + this._serviceSelector = new Mock(); } [Fact] @@ -378,7 +380,7 @@ public sealed class PromptTemplateEngineTests this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((pluginName, functionName, variables, cancellationToken) => { - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables); return function.InvokeAsync(context, null, cancellationToken); }); } @@ -388,7 +390,7 @@ public sealed class PromptTemplateEngineTests this._functionRunner.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((pluginName, functionName, variables, cancellationToken) => { - var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, variables); + var context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, variables); var function = functions.First(f => f.PluginName == functionName); return function.InvokeAsync(context, null, cancellationToken); @@ -400,6 +402,7 @@ public sealed class PromptTemplateEngineTests return new SKContext( this._functionRunner.Object, this._serviceProvider.Object, + this._serviceSelector.Object, this._variables, this._functions.Object); } diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs index 8aff332f21..d574147429 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Action/ActionPlannerTests.cs @@ -165,11 +165,12 @@ This plan uses the `GitHubPlugin.PullsList` function to list the open pull reque } var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); var kernel = new Mock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(testPlanString), functions.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(testPlanString), functions.Object); - var context = new SKContext(functionRunner.Object, serviceProvider.Object, functions: functions.Object); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, functions: functions.Object); var mockFunctionFlowFunction = new Mock(); mockFunctionFlowFunction.Setup(x => x.InvokeAsync( diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs index edcf0554af..e755b93005 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Extensions/ReadOnlyFunctionCollectionExtensionsTests.cs @@ -59,9 +59,10 @@ public class ReadOnlyFunctionCollectionExtensionsTests var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables); var config = InitializeConfig(t); var semanticQuery = "test"; @@ -139,9 +140,10 @@ public class ReadOnlyFunctionCollectionExtensionsTests var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables, functions.Object); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables, functions.Object); var config = InitializeConfig(t); var semanticQuery = "test"; @@ -210,9 +212,10 @@ public class ReadOnlyFunctionCollectionExtensionsTests var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables, functions.Object); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables, functions.Object); var config = InitializeConfig(t); config.SemanticMemoryConfig = new() { RelevancyThreshold = 0.78, Memory = memory.Object }; var semanticQuery = "test"; @@ -248,6 +251,7 @@ public class ReadOnlyFunctionCollectionExtensionsTests var kernel = new Mock(); var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); var variables = new ContextVariables(); var functions = new FunctionCollection(); @@ -272,7 +276,7 @@ public class ReadOnlyFunctionCollectionExtensionsTests .Returns(asyncEnumerable); // Arrange GetAvailableFunctionsAsync parameters - var context = new SKContext(functionRunner.Object, serviceProvider.Object, variables); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables); var config = InitializeConfig(t); config.SemanticMemoryConfig = new() { RelevancyThreshold = 0.78, Memory = memory.Object }; var semanticQuery = "test"; diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs index 263a75a8fc..232e0b6498 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlanParserTests.cs @@ -40,9 +40,10 @@ public class SequentialPlanParserTests private SKContext CreateSKContext( IFunctionRunner functionRunner, IAIServiceProvider serviceProvider, + IAIServiceSelector serviceSelector, ContextVariables? variables = null) { - return new SKContext(functionRunner, serviceProvider, variables); + return new SKContext(functionRunner, serviceProvider, serviceSelector, variables); } private static Mock CreateMockFunction(FunctionView functionView, string result = "") @@ -62,12 +63,13 @@ public class SequentialPlanParserTests var functionRunnerMock = new Mock(); var serviceProviderMock = new Mock(); + var serviceSelector = new Mock(); // For Create kernelMock.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, skills, loggerFactory, culture) => { - return this.CreateSKContext(functionRunnerMock.Object, serviceProviderMock.Object, contextVariables); + return this.CreateSKContext(functionRunnerMock.Object, serviceProviderMock.Object, serviceSelector.Object, contextVariables); }); var functionsView = new List(); @@ -80,7 +82,7 @@ public class SequentialPlanParserTests var mockFunction = CreateMockFunction(functionView); functionsView.Add(functionView); - var result = this.CreateSKContext(functionRunnerMock.Object, serviceProviderMock.Object); + var result = this.CreateSKContext(functionRunnerMock.Object, serviceProviderMock.Object, serviceSelector.Object); result.Variables.Update(resultString); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) .ReturnsAsync(new FunctionResult(name, pluginName, result)); @@ -91,8 +93,7 @@ public class SequentialPlanParserTests It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), - It.IsAny() + It.IsAny() )).Returns(mockFunction.Object); } else diff --git a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs index 200f4088d9..2506fb1f27 100644 --- a/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs +++ b/dotnet/src/Planners/Planners.Core.UnitTests/Sequential/SequentialPlannerTests.cs @@ -62,6 +62,7 @@ public sealed class SequentialPlannerTests functions.Setup(x => x.GetFunctionViews()).Returns(functionsView); var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); kernel.Setup(x => x.LoggerFactory).Returns(new Mock().Object); var expectedFunctions = input.Select(x => x.name).ToList(); @@ -70,11 +71,13 @@ public sealed class SequentialPlannerTests var context = new SKContext( functionRunner.Object, serviceProvider.Object, + serviceSelector.Object, new ContextVariables()); var returnContext = new SKContext( functionRunner.Object, serviceProvider.Object, + serviceSelector.Object, new ContextVariables()); var planString = @@ -152,15 +155,16 @@ public sealed class SequentialPlannerTests // Arrange var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); var kernel = new Mock(); var functions = new Mock(); functions.Setup(x => x.GetFunctionViews()).Returns(new List()); var planString = "notvalid<"; - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(planString)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(planString)); - var context = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables()); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables()); var mockFunctionFlowFunction = new Mock(); mockFunctionFlowFunction.Setup(x => x.InvokeAsync( diff --git a/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs b/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs index ac239f2381..f99f48fb7d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Orchestration/SKContext.cs @@ -62,13 +62,19 @@ public sealed class SKContext /// /// AI service provider /// - public IAIServiceProvider ServiceProvider { get; } + internal IAIServiceProvider ServiceProvider { get; } + + /// + /// AIService selector implementation + /// + internal IAIServiceSelector ServiceSelector { get; } /// /// Constructor for the context. /// /// Function runner reference /// AI service provider + /// AI service selector /// Context variables to include in context. /// Functions to include in context. /// Logger factory to be used in context @@ -76,6 +82,7 @@ public sealed class SKContext internal SKContext( IFunctionRunner functionRunner, IAIServiceProvider serviceProvider, + IAIServiceSelector serviceSelector, ContextVariables? variables = null, IReadOnlyFunctionCollection? functions = null, ILoggerFactory? loggerFactory = null, @@ -85,6 +92,7 @@ public sealed class SKContext this.Runner = functionRunner; this.ServiceProvider = serviceProvider; + this.ServiceSelector = serviceSelector; this.Variables = variables ?? new(); this.Functions = functions ?? NullReadOnlyFunctionCollection.Instance; this.LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; @@ -120,6 +128,7 @@ public sealed class SKContext return new SKContext( this.Runner, this.ServiceProvider, + this.ServiceSelector, variables ?? this.Variables.Clone(), functions ?? this.Functions, this.LoggerFactory, diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs b/dotnet/src/SemanticKernel.Abstractions/Services/IAIServiceSelector.cs similarity index 82% rename from dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs rename to dotnet/src/SemanticKernel.Abstractions/Services/IAIServiceSelector.cs index 3325f21ee0..f35a52ffaf 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/IAIServiceSelector.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Services/IAIServiceSelector.cs @@ -18,8 +18,9 @@ public interface IAIServiceSelector /// The returned value is a tuple containing instances of and ///
/// Type of AI service to return + /// Rendered prompt /// AI service provider /// Collection of model settings /// - (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService; + (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService; } diff --git a/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs b/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs index d34f80b687..2bae6043c7 100644 --- a/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/Extensions/KernelSemanticFunctionExtensions.cs @@ -29,14 +29,12 @@ public static class KernelSemanticFunctionExtensions /// Name of the semantic function. The name can contain only alphanumeric chars + underscore. /// Prompt template configuration. /// Prompt template. - /// Optional, AI service selector. /// A C# function wrapping AI logic, usually defined with natural language public static ISKFunction RegisterSemanticFunction( this IKernel kernel, string functionName, PromptTemplateConfig promptTemplateConfig, - IPromptTemplate promptTemplate, - IAIServiceSelector? serviceSelector = null) + IPromptTemplate promptTemplate) { return kernel.RegisterSemanticFunction(FunctionCollection.GlobalFunctionsPluginName, functionName, promptTemplateConfig, promptTemplate); } @@ -49,15 +47,13 @@ public static class KernelSemanticFunctionExtensions /// Name of the semantic function. The name can contain only alphanumeric chars + underscore. /// Prompt template configuration. /// Prompt template. - /// Optional, AI service selector. /// A C# function wrapping AI logic, usually defined with natural language public static ISKFunction RegisterSemanticFunction( this IKernel kernel, string pluginName, string functionName, PromptTemplateConfig promptTemplateConfig, - IPromptTemplate promptTemplate, - IAIServiceSelector? serviceSelector = null) + IPromptTemplate promptTemplate) { // Future-proofing the name not to contain special chars Verify.ValidFunctionName(functionName); @@ -279,15 +275,13 @@ public static class KernelSemanticFunctionExtensions string pluginName, string functionName, PromptTemplateConfig promptTemplateConfig, - IPromptTemplate promptTemplate, - IAIServiceSelector? serviceSelector = null) + IPromptTemplate promptTemplate) { return SemanticFunction.FromSemanticConfig( pluginName, functionName, promptTemplateConfig, promptTemplate, - serviceSelector, kernel.LoggerFactory ); } diff --git a/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs b/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs index 47d310f3c3..05a7a05e64 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/DelegatingAIServiceSelector.cs @@ -18,7 +18,7 @@ internal class DelegatingAIServiceSelector : IAIServiceSelector internal AIRequestSettings? RequestSettings { get; set; } /// - public (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + public (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService { return ((T?)this.ServiceFactory?.Invoke() ?? serviceProvider.GetService(null), this.RequestSettings ?? modelSettings?[0]); } diff --git a/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs b/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs index f1f8bf6432..0ce1c06f67 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/OrderedIAIServiceSelector.cs @@ -15,7 +15,7 @@ namespace Microsoft.SemanticKernel.Functions; internal class OrderedIAIServiceSelector : IAIServiceSelector { /// - public (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService + public (T?, AIRequestSettings?) SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService { if (modelSettings is null || modelSettings.Count == 0) { diff --git a/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs b/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs index 43e2923bc0..8c14419c17 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs @@ -51,7 +51,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable /// Name of the function to create. /// Prompt template configuration. /// Prompt template. - /// AI service selector /// The to use for logging. If null, no logging will be performed. /// The to monitor for cancellation requests. The default is . /// SK function instance. @@ -60,7 +59,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable string functionName, PromptTemplateConfig promptTemplateConfig, IPromptTemplate promptTemplate, - IAIServiceSelector? serviceSelector = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default) { @@ -72,7 +70,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable description: promptTemplateConfig.Description, pluginName: pluginName, functionName: functionName, - serviceSelector: serviceSelector, loggerFactory: loggerFactory ) { @@ -96,8 +93,7 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable { this.AddDefaultValues(context.Variables); - (var textCompletion, var defaultRequestSettings) = this._serviceSelector.SelectAIService(context.ServiceProvider, this._modelSettings); - return await this.RunPromptAsync(textCompletion, requestSettings ?? defaultRequestSettings, context, cancellationToken).ConfigureAwait(false); + return await this.RunPromptAsync(requestSettings, context, cancellationToken).ConfigureAwait(false); } /// @@ -124,7 +120,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable string pluginName, string functionName, string description, - IAIServiceSelector? serviceSelector = null, ILoggerFactory? loggerFactory = null) { Verify.NotNull(template); @@ -140,8 +135,6 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable this.PluginName = pluginName; this.Description = description; - this._serviceSelector = serviceSelector ?? new OrderedIAIServiceSelector(); - this._view = new(() => new(functionName, pluginName, description, this.Parameters)); } @@ -150,7 +143,7 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable private static readonly JsonSerializerOptions s_toStringStandardSerialization = new(); private static readonly JsonSerializerOptions s_toStringIndentedSerialization = new() { WriteIndented = true }; private readonly ILogger _logger; - private IAIServiceSelector _serviceSelector; + private IAIServiceSelector? _serviceSelector; public List? _modelSettings; private readonly Lazy _view; public IPromptTemplate _promptTemplate { get; } @@ -177,19 +170,20 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable } private async Task RunPromptAsync( - ITextCompletion? client, AIRequestSettings? requestSettings, SKContext context, CancellationToken cancellationToken) { - Verify.NotNull(client); - FunctionResult result; try { string renderedPrompt = await this._promptTemplate.RenderAsync(context, cancellationToken).ConfigureAwait(false); - IReadOnlyList completionResults = await client.GetCompletionsAsync(renderedPrompt, requestSettings, cancellationToken).ConfigureAwait(false); + // For backward compatibility, use the service selector from the class if it exists, otherwise use the one from the context + var serviceSelector = this._serviceSelector ?? context.ServiceSelector; + (var textCompletion, var defaultRequestSettings) = serviceSelector.SelectAIService(renderedPrompt, context.ServiceProvider, this._modelSettings); + Verify.NotNull(textCompletion); + IReadOnlyList completionResults = await textCompletion.GetCompletionsAsync(renderedPrompt, requestSettings ?? defaultRequestSettings, cancellationToken).ConfigureAwait(false); string completion = await GetCompletionsResultContentAsync(completionResults, cancellationToken).ConfigureAwait(false); // Update the result with the completion diff --git a/dotnet/src/SemanticKernel.Core/Kernel.cs b/dotnet/src/SemanticKernel.Core/Kernel.cs index 3999733678..5338bb25a8 100644 --- a/dotnet/src/SemanticKernel.Core/Kernel.cs +++ b/dotnet/src/SemanticKernel.Core/Kernel.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Events; +using Microsoft.SemanticKernel.Functions; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Orchestration; @@ -63,15 +64,18 @@ public sealed class Kernel : IKernel, IDisposable /// AI Service Provider /// Prompt template engine /// Semantic text Memory - /// + /// HTTP handler factory /// The to use for logging. If null, no logging will be performed. + /// AI service selector public Kernel( IFunctionCollection functionCollection, IAIServiceProvider aiServiceProvider, IPromptTemplateEngine promptTemplateEngine, ISemanticTextMemory memory, IDelegatingHandlerFactory httpHandlerFactory, - ILoggerFactory? loggerFactory) + ILoggerFactory? loggerFactory, + IAIServiceSelector? serviceSelector = null + ) { loggerFactory ??= NullLoggerFactory.Instance; @@ -82,6 +86,7 @@ public sealed class Kernel : IKernel, IDisposable this._aiServiceProvider = aiServiceProvider; this._promptTemplateEngine = promptTemplateEngine; this._functionCollection = functionCollection; + this._aiServiceSelector = serviceSelector ?? new OrderedIAIServiceSelector(); this._logger = loggerFactory.CreateLogger(typeof(Kernel)); } @@ -176,6 +181,7 @@ repeat: return new SKContext( new FunctionRunner(this), this._aiServiceProvider, + this._aiServiceSelector, variables, functions ?? this.Functions, loggerFactory ?? this.LoggerFactory, @@ -212,6 +218,7 @@ repeat: private ISemanticTextMemory _memory; private readonly IPromptTemplateEngine _promptTemplateEngine; private readonly IAIServiceProvider _aiServiceProvider; + private readonly IAIServiceSelector _aiServiceSelector; private readonly ILogger _logger; /// diff --git a/dotnet/src/SemanticKernel.Core/KernelBuilder.cs b/dotnet/src/SemanticKernel.Core/KernelBuilder.cs index 67609299ed..201397cf59 100644 --- a/dotnet/src/SemanticKernel.Core/KernelBuilder.cs +++ b/dotnet/src/SemanticKernel.Core/KernelBuilder.cs @@ -28,6 +28,7 @@ public sealed class KernelBuilder private IDelegatingHandlerFactory _httpHandlerFactory = NullHttpHandlerFactory.Instance; private IPromptTemplateEngine? _promptTemplateEngine; private readonly AIServiceCollection _aiServices = new(); + private IAIServiceSelector? _serviceSelector; private static bool s_promptTemplateEngineInitialized = false; private static Type? s_promptTemplateEngineType = null; @@ -54,7 +55,8 @@ public sealed class KernelBuilder this._promptTemplateEngine ?? this.CreateDefaultPromptTemplateEngine(this._loggerFactory), this._memoryFactory.Invoke(), this._httpHandlerFactory, - this._loggerFactory + this._loggerFactory, + this._serviceSelector ); // TODO: decouple this from 'UseMemory' kernel extension @@ -250,6 +252,15 @@ public sealed class KernelBuilder return this; } + /// + /// Adds a to the builder + /// + public KernelBuilder WithAIServiceSelector(IAIServiceSelector serviceSelector) + { + this._serviceSelector = serviceSelector; + return this; + } + /// /// Create a default prompt template engine. /// diff --git a/dotnet/src/SemanticKernel.Core/TemplateEngine/PromptTemplateConfig.cs b/dotnet/src/SemanticKernel.Core/TemplateEngine/PromptTemplateConfig.cs index 9e5817d27c..b3588903b4 100644 --- a/dotnet/src/SemanticKernel.Core/TemplateEngine/PromptTemplateConfig.cs +++ b/dotnet/src/SemanticKernel.Core/TemplateEngine/PromptTemplateConfig.cs @@ -102,7 +102,7 @@ public class PromptTemplateConfig /// TODO: use enum [JsonPropertyName("type")] [JsonPropertyOrder(5)] - [Obsolete("Type property is no longer used. This will be removed in a future release.")] + [Obsolete("Type property is no longer required. This will be removed in a future release.")] public string Type { get; set; } = "completion"; /// @@ -111,7 +111,7 @@ public class PromptTemplateConfig [JsonPropertyName("completion")] [JsonPropertyOrder(6)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [Obsolete("Completion is no longer no longer supported. Use PromptTemplateConfig.Models collection instead. This will be removed in a future release.")] + [Obsolete("Completion is no longer no longer supported. Use PromptTemplateConfig.ModelSettings collection instead. This will be removed in a future release.")] public AIRequestSettings? Completion { get { return this.GetDefaultRequestSettings(); } @@ -130,7 +130,7 @@ public class PromptTemplateConfig [JsonPropertyName("default_services")] [JsonPropertyOrder(7)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [Obsolete("DefaultServices property is no longer used. This will be removed in a future release.")] + [Obsolete("DefaultServices property is not being used. This will be removed in a future release.")] public List DefaultServices { get; set; } = new(); #endregion diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs index 5159695c30..c230a440e3 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/OrderedIAIServiceConfigurationProviderTests.cs @@ -18,6 +18,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItThrowsAnSKExceptionForNoServices() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); var serviceProvider = serviceCollection.Build(); var modelSettings = new List(); @@ -25,13 +26,14 @@ public class OrderedIAIServiceConfigurationProviderTests // Act // Assert - Assert.Throws(() => configurationProvider.SelectAIService(serviceProvider, modelSettings)); + Assert.Throws(() => configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings)); } [Fact] public void ItGetsAIServiceConfigurationForSingleAIService() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService(new AIService()); var serviceProvider = serviceCollection.Build(); @@ -39,7 +41,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.NotNull(aiService); @@ -50,6 +52,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItGetsAIServiceConfigurationForSingleTextCompletion() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService(new TextCompletion()); var serviceProvider = serviceCollection.Build(); @@ -57,7 +60,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.NotNull(aiService); @@ -68,6 +71,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItAIServiceConfigurationForTextCompletionByServiceId() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion()); @@ -76,7 +80,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.NotNull(aiService); @@ -87,6 +91,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItThrowsAnSKExceptionForNotFoundService() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion()); @@ -99,13 +104,14 @@ public class OrderedIAIServiceConfigurationProviderTests // Act // Assert - Assert.Throws(() => configurationProvider.SelectAIService(serviceProvider, modelSettings)); + Assert.Throws(() => configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings)); } [Fact] public void ItUsesDefaultServiceForNullModelSettings() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion(), true); @@ -113,7 +119,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, null); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, null); // Assert Assert.Equal(serviceProvider.GetService("service2"), aiService); @@ -124,6 +130,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItUsesDefaultServiceForEmptyModelSettings() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion(), true); @@ -132,7 +139,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.Equal(serviceProvider.GetService("service2"), aiService); @@ -143,6 +150,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItUsesDefaultServiceAndSettings() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion(), true); @@ -154,7 +162,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.Equal(serviceProvider.GetService("service2"), aiService); @@ -165,6 +173,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItUsesDefaultServiceAndSettingsEmptyServiceId() { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion(), true); @@ -176,7 +185,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.Equal(serviceProvider.GetService("service2"), aiService); @@ -191,6 +200,7 @@ public class OrderedIAIServiceConfigurationProviderTests public void ItGetsAIServiceConfigurationByOrder(string[] serviceIds, string expectedServiceId) { // Arrange + var renderedPrompt = "Hello AI, what can you do for me?"; var serviceCollection = new AIServiceCollection(); serviceCollection.SetService("service1", new TextCompletion()); serviceCollection.SetService("service2", new TextCompletion()); @@ -204,7 +214,7 @@ public class OrderedIAIServiceConfigurationProviderTests var configurationProvider = new OrderedIAIServiceSelector(); // Act - (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(serviceProvider, modelSettings); + (var aiService, var defaultRequestSettings) = configurationProvider.SelectAIService(renderedPrompt, serviceProvider, modelSettings); // Assert Assert.Equal(serviceProvider.GetService(expectedServiceId), aiService); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs index dfb60d5716..0106b73944 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SKContextTests.cs @@ -23,7 +23,7 @@ public class SKContextTests { // Arrange var variables = new ContextVariables(); - var target = new SKContext(new Mock().Object, new Mock().Object, variables); + var target = new SKContext(new Mock().Object, new Mock().Object, new Mock().Object, variables); variables.Set("foo1", "bar1"); // Act @@ -47,8 +47,8 @@ public class SKContextTests // Arrange IDictionary functions = KernelBuilder.Create().ImportFunctions(new Parrot(), "test"); this._functions.Setup(x => x.GetFunction("func")).Returns(functions["say"]); - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(this._functions.Object); - var target = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(), this._functions.Object); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(this._functions.Object); + var target = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(), this._functions.Object); Assert.NotNull(target.Functions); // Act @@ -61,22 +61,23 @@ public class SKContextTests Assert.Equal("ciao", result.GetValue()); } - private (Mock kernelMock, Mock functionRunnerMock, Mock serviceProviderMock) SetupKernelMock(IReadOnlyFunctionCollection? functions = null) + private (Mock kernelMock, Mock functionRunnerMock, Mock serviceProviderMock, Mock serviceSelectorMock) SetupKernelMock(IReadOnlyFunctionCollection? functions = null) { functions ??= new Mock().Object; var kernel = new Mock(); var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); kernel.SetupGet(x => x.Functions).Returns(functions); kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, skills, loggerFactory, culture) => { - return new SKContext(functionRunner.Object, serviceProvider.Object, contextVariables); + return new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, contextVariables); }); - return (kernel, functionRunner, serviceProvider); + return (kernel, functionRunner, serviceProvider, serviceSelector); } private sealed class Parrot diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs index b717eb398c..a0f5e523dc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/SKFunctionTests2.cs @@ -1046,10 +1046,12 @@ public sealed class SKFunctionTests2 { var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); return new SKContext( functionRunner.Object, serviceProvider.Object, + serviceSelector.Object, new ContextVariables(input) ); } diff --git a/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs b/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs index 62d455fd34..949d9fda28 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Orchestration/FunctionResultTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Services; using Moq; @@ -15,10 +16,11 @@ public class FunctionResultTests { private readonly Mock _functionRunner = new(); private readonly Mock _serviceProvider = new(); + private readonly Mock _serviceSelector = new(); private SKContext CreateContext() { - return new SKContext(this._functionRunner.Object, this._serviceProvider.Object); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs b/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs index 17b68facf2..2181cd6e8a 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Orchestration/KernelResultTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Services; using Moq; @@ -17,11 +18,12 @@ public class KernelResultTests { private readonly Mock _functionRunner = new(); private readonly Mock _serviceProvider = new(); + private readonly Mock _serviceSelector = new(); private readonly SKContext _context; public KernelResultTests() { - this._context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object); + this._context = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs index 5791f6e236..b1c3bcb934 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanSerializationTests.cs @@ -20,6 +20,7 @@ public sealed class PlanSerializationTests { private readonly Mock _functionRunner = new(); private readonly Mock _serviceProvider = new(); + private readonly Mock _serviceSelector = new(); [Fact] public void CanSerializePlan() @@ -89,7 +90,7 @@ public sealed class PlanSerializationTests // Arrange Mocks var functions = new Mock(); - var returnContext = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -122,7 +123,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); @@ -157,7 +158,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); @@ -192,7 +193,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); @@ -226,7 +227,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); @@ -264,10 +265,10 @@ public sealed class PlanSerializationTests kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, functions, loggerFactory, culture) => { - return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, contextVariables, functions); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, contextVariables, functions); }); - var returnContext = new SKContext(functionRunner.Object, this._serviceProvider.Object, new ContextVariables(stepOutput) + var returnContext = new SKContext(functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); var mockFunction = new Mock(); @@ -279,7 +280,7 @@ public sealed class PlanSerializationTests this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((function, variables, ct) => { - var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); + var c = new SKContext(new Mock().Object, this._serviceProvider.Object, this._serviceSelector.Object, variables); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input); var functionResult = new FunctionResult(function.Name, function.PluginName, returnContext); return Task.FromResult(functionResult); @@ -336,11 +337,11 @@ public sealed class PlanSerializationTests kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, functions, loggerFactory, culture) => { - return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, contextVariables); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, contextVariables); }); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); @@ -358,7 +359,7 @@ public sealed class PlanSerializationTests this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((function, variables, ct) => { - var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); + var c = new SKContext(new Mock().Object, this._serviceProvider.Object, this._serviceSelector.Object, variables); c.Variables.TryGetValue("variables", out string? v); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v); @@ -425,14 +426,14 @@ public sealed class PlanSerializationTests kernel.SetupGet(x => x.Functions).Returns(functions.Object); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, functions, loggerFactory, culture) => { - return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, contextVariables); + return new SKContext(this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, contextVariables); }); var mockFunction = new Mock(); @@ -461,7 +462,7 @@ public sealed class PlanSerializationTests this._functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((function, variables, ct) => { - var c = new SKContext(new Mock().Object, this._serviceProvider.Object, variables); + var c = new SKContext(new Mock().Object, this._serviceProvider.Object, this._serviceSelector.Object, variables); c.Variables.TryGetValue("variables", out string? v); returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v); @@ -489,7 +490,7 @@ public sealed class PlanSerializationTests cv.Update(string.Empty); var nextContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables() ); plan = Plan.FromJson(serializedPlan1, functions.Object); @@ -524,7 +525,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); @@ -579,7 +580,7 @@ public sealed class PlanSerializationTests var functions = new Mock(); var returnContext = new SKContext( - this._functionRunner.Object, this._serviceProvider.Object, + this._functionRunner.Object, this._serviceProvider.Object, this._serviceSelector.Object, new ContextVariables(stepOutput) ); diff --git a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs index 28b78b558d..ca7b2afa55 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Planning/PlanTests.cs @@ -35,7 +35,7 @@ public sealed class PlanTests public async Task CanExecutePlanAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); var goal = "Write a poem or joke and send it in an e-mail to Kai."; var plan = new Plan(goal); @@ -56,8 +56,9 @@ public sealed class PlanTests var plan = new Plan(goal); var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); - var context = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables("Some input")); + var context = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables("Some input")); // Act var result = await plan.InvokeAsync(context); @@ -87,9 +88,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -120,9 +121,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -153,9 +154,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -186,9 +187,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -219,9 +220,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -252,9 +253,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput) + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput) ); var mockFunction = new Mock(); @@ -292,9 +293,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -341,9 +342,9 @@ public sealed class PlanTests var plan = new Plan(goal); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, new ContextVariables(stepOutput)); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, new ContextVariables(stepOutput)); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -369,9 +370,9 @@ public sealed class PlanTests // Arrange var logger = new Mock(); var functions = new Mock(); - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -395,9 +396,9 @@ public sealed class PlanTests var subPlan = new Plan("Write a poem or joke"); // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var childFunction1 = new Mock(); childFunction1.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -476,9 +477,9 @@ public sealed class PlanTests public async Task CanExecutePlanWithOneStepAndStateAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -504,17 +505,17 @@ public sealed class PlanTests public async Task CanExecutePlanWithStateAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(async (function, variables, ct) => { - var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); + var c = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); return functionResult; }); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -547,9 +548,9 @@ public sealed class PlanTests public async Task CanExecutePlanWithCustomContextAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -578,7 +579,7 @@ public sealed class PlanTests plan.State.Set("input", "Cleopatra"); plan.State.Set("type", "poem"); - var contextOverride = new SKContext(functionRunner.Object, serviceProvider.Object); + var contextOverride = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); contextOverride.Variables.Set("type", "joke"); contextOverride.Variables.Update("Medusa"); @@ -596,9 +597,9 @@ public sealed class PlanTests public async Task CanExecutePlanWithCustomStateAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var mockFunction = new Mock(); mockFunction.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -648,7 +649,7 @@ public sealed class PlanTests planStep.Parameters.Set("input", "Cleopatra"); planStep.Parameters.Set("type", "poem"); plan.AddSteps(planStep); - var contextOverride = new SKContext(functionRunner.Object, serviceProvider.Object); + var contextOverride = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); contextOverride.Variables.Set("type", "joke"); contextOverride.Variables.Update("Medusa"); // context input will not override parameters @@ -666,17 +667,17 @@ public sealed class PlanTests public async Task CanExecutePlanWithJoinedResultAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(async (function, variables, ct) => { - var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); + var c = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); return functionResult; }); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var outlineMock = new Mock(); outlineMock.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -796,9 +797,9 @@ Previously:Outline section #1 of 3: Here is a 3 chapter outline about NovelOutli public async Task CanExecutePlanWithExpandedAsync() { // Arrange - var (kernel, functionRunner, serviceProvider) = this.SetupKernelMock(); + var (kernel, functionRunner, serviceProvider, serviceSelector) = this.SetupKernelMock(); - var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object); + var returnContext = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object); var functionMock = new Mock(); functionMock.Setup(x => x.InvokeAsync(It.IsAny(), null, It.IsAny())) @@ -827,29 +828,30 @@ Previously:Outline section #1 of 3: Here is a 3 chapter outline about NovelOutli Assert.Equal(expected, result.GetValue()); } - private (Mock kernelMock, Mock functionRunnerMock, Mock serviceProviderMock) SetupKernelMock(IFunctionCollection? functions = null) + private (Mock kernelMock, Mock functionRunnerMock, Mock serviceProviderMock, Mock serviceSelectorMock) SetupKernelMock(IFunctionCollection? functions = null) { functions ??= new Mock().Object; var kernel = new Mock(); var functionRunner = new Mock(); var serviceProvider = new Mock(); + var serviceSelector = new Mock(); kernel.SetupGet(x => x.Functions).Returns(functions); kernel.Setup(k => k.CreateNewContext(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((contextVariables, skills, loggerFactory, culture) => { - return new SKContext(functionRunner.Object, serviceProvider.Object, contextVariables, functions); + return new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, contextVariables, functions); }); functionRunner.Setup(k => k.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(async (function, variables, ct) => { - var c = new SKContext(functionRunner.Object, serviceProvider.Object, variables); + var c = new SKContext(functionRunner.Object, serviceProvider.Object, serviceSelector.Object, variables); var functionResult = await function.InvokeAsync(c, cancellationToken: ct); return functionResult; }); - return (kernel, functionRunner, serviceProvider); + return (kernel, functionRunner, serviceProvider, serviceSelector); } } From 85fe86307f1933e2bf5826692109984b061d7bb6 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:42:05 +0000 Subject: [PATCH 17/24] .Net: Bump version to 1.0.0-beta4 (#3343) ### Motivation and Context Bump version to 1.0.0-beta4 for weekly release ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 62e7e6cfc7..1b1145588e 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -1,7 +1,7 @@ - 1.0.0-beta3 + 1.0.0-beta4 Debug;Release;Publish true From 7890427b09b0e529caa641088298599023614f03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:10:30 +0000 Subject: [PATCH 18/24] Python: Bump pinecone-client from 2.2.2 to 2.2.4 in /python (#3266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pinecone-client](https://github.com/pinecone-io/pinecone-python-client) from 2.2.2 to 2.2.4.
Release notes

Sourced from pinecone-client's releases.

Release v2.2.4

What's Changed

New Contributors

Full Changelog: https://github.com/pinecone-io/pinecone-python-client/compare/v2.2.2...v2.2.4

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pinecone-client&package-manager=pip&previous-version=2.2.2&new-version=2.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- python/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index 96989acee0..5d16594d41 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -2888,13 +2888,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "pinecone-client" -version = "2.2.2" +version = "2.2.4" description = "Pinecone client and SDK" optional = false python-versions = ">=3.8" files = [ - {file = "pinecone-client-2.2.2.tar.gz", hash = "sha256:391fe413754efd4e0ef00154b44271d63c4cdd4bedf088d23111a5725d863210"}, - {file = "pinecone_client-2.2.2-py3-none-any.whl", hash = "sha256:21fddb752668efee4d3c6b706346d9580e36a8b06b8d97afd60bd33ef2536e7e"}, + {file = "pinecone-client-2.2.4.tar.gz", hash = "sha256:2c1cc1d6648b2be66e944db2ffa59166a37b9164d1135ad525d9cd8b1e298168"}, + {file = "pinecone_client-2.2.4-py3-none-any.whl", hash = "sha256:5bf496c01c2f82f4e5c2dc977cc5062ecd7168b8ed90743b09afcc8c7eb242ec"}, ] [package.dependencies] @@ -2909,7 +2909,7 @@ typing-extensions = ">=3.7.4" urllib3 = ">=1.21.1" [package.extras] -grpc = ["googleapis-common-protos (>=1.53.0)", "grpc-gateway-protoc-gen-openapiv2 (==0.1.0)", "grpcio (>=1.44.0)", "lz4 (>=3.1.3)", "protobuf (>=3.19.5,<3.20.0)"] +grpc = ["googleapis-common-protos (>=1.53.0)", "grpc-gateway-protoc-gen-openapiv2 (==0.1.0)", "grpcio (>=1.44.0)", "lz4 (>=3.1.3)", "protobuf (>=3.20.0,<3.21.0)"] [[package]] name = "pkgutil-resolve-name" From 6ac9ee3493023e92d46cd22636af3918520af990 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:22:32 +0000 Subject: [PATCH 19/24] .Net: Bump Grpc.Net.Client from 2.57.0 to 2.58.0 in /dotnet (#3267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [Grpc.Net.Client](https://github.com/grpc/grpc-dotnet) from 2.57.0 to 2.58.0.
Release notes

Sourced from Grpc.Net.Client's releases.

Release v2.58.0

What's Changed

Full Changelog: https://github.com/grpc/grpc-dotnet/compare/v2.57.0...v2.58.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Grpc.Net.Client&package-manager=nuget&previous-version=2.57.0&new-version=2.58.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index b8ae82044b..984874da0c 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -55,7 +55,7 @@ - + From fe353114b35170cba22dbc1f76435d2006d86ca7 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:45:40 +0000 Subject: [PATCH 20/24] .Net: Remove the Oobabooga AI connector (#3350) ### Motivation and Context Oobabooga AI connector has now moved to https://github.com/MyIntelligenceAgency/semantic-fleet so removing the copy in the SK repo ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- dotnet/SK-dotnet.sln | 9 - .../Connectors.AI.Oobabooga.csproj | 28 - .../Connectors.AI.Oobabooga/README.md | 3 + .../TextCompletion/OobaboogaTextCompletion.cs | 481 ------------------ .../TextCompletion/TextCompletionRequest.cs | 179 ------- .../TextCompletion/TextCompletionResponse.cs | 33 -- .../TextCompletion/TextCompletionResult.cs | 30 -- .../TextCompletionStreamingResponse.cs | 41 -- .../TextCompletionStreamingResult.cs | 68 --- .../Connectors.UnitTests/ConnectedClient.cs | 25 - .../Connectors.UnitTests.csproj | 7 - .../Oobabooga/OobaboogaTestHelper.cs | 44 -- .../Oobabooga/OobaboogaWebSocketTestServer.cs | 63 --- .../TestData/completion_test_response.json | 9 - .../completion_test_streaming_response.json | 5 - .../OobaboogaTextCompletionTests.cs | 401 --------------- .../WebSocketTestServer.cs | 229 --------- .../Connectors.UnitTests/XunitLogger.cs | 40 -- .../Oobabooga/OobaboogaTextCompletionTests.cs | 111 ---- .../IntegrationTests/IntegrationTests.csproj | 1 - 20 files changed, 3 insertions(+), 1804 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/Connectors.AI.Oobabooga.csproj create mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/README.md delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/OobaboogaTextCompletion.cs delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionRequest.cs delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResponse.cs delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResult.cs delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResponse.cs delete mode 100644 dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResult.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/ConnectedClient.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaTestHelper.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaWebSocketTestServer.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_streaming_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TextCompletion/OobaboogaTextCompletionTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/WebSocketTestServer.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/XunitLogger.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/Oobabooga/OobaboogaTextCompletionTests.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 45273e9ad1..86b0ceb3d3 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -140,8 +140,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Core", "src\Plugins EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCalcPlugins", "samples\NCalcPlugins\NCalcPlugins.csproj", "{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AI.Oobabooga", "src\Connectors\Connectors.AI.Oobabooga\Connectors.AI.Oobabooga.csproj", "{677F1381-7830-4115-9C1A-58B282629DC6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationInsightsExample", "samples\ApplicationInsightsExample\ApplicationInsightsExample.csproj", "{C754950A-E16C-4F96-9CC7-9328E361B5AF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Kusto", "src\Connectors\Connectors.Memory.Kusto\Connectors.Memory.Kusto.csproj", "{E07608CC-D710-4655-BB9E-D22CF3CDD193}" @@ -348,12 +346,6 @@ Global {E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Publish|Any CPU.ActiveCfg = Release|Any CPU {E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Release|Any CPU.Build.0 = Release|Any CPU - {677F1381-7830-4115-9C1A-58B282629DC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {677F1381-7830-4115-9C1A-58B282629DC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {677F1381-7830-4115-9C1A-58B282629DC6}.Publish|Any CPU.ActiveCfg = Publish|Any CPU - {677F1381-7830-4115-9C1A-58B282629DC6}.Publish|Any CPU.Build.0 = Publish|Any CPU - {677F1381-7830-4115-9C1A-58B282629DC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {677F1381-7830-4115-9C1A-58B282629DC6}.Release|Any CPU.Build.0 = Release|Any CPU {C754950A-E16C-4F96-9CC7-9328E361B5AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C754950A-E16C-4F96-9CC7-9328E361B5AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {C754950A-E16C-4F96-9CC7-9328E361B5AF}.Publish|Any CPU.ActiveCfg = Release|Any CPU @@ -460,7 +452,6 @@ Global {3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132} {E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA} = {FA3720F1-C99A-49B2-9577-A940257098BF} - {677F1381-7830-4115-9C1A-58B282629DC6} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {C754950A-E16C-4F96-9CC7-9328E361B5AF} = {FA3720F1-C99A-49B2-9577-A940257098BF} {E07608CC-D710-4655-BB9E-D22CF3CDD193} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {10E4B697-D4E8-468D-872D-49670FD150FB} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633} diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/Connectors.AI.Oobabooga.csproj b/dotnet/src/Connectors/Connectors.AI.Oobabooga/Connectors.AI.Oobabooga.csproj deleted file mode 100644 index f55b818e10..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/Connectors.AI.Oobabooga.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Microsoft.SemanticKernel.Connectors.AI.Oobabooga - $(AssemblyName) - netstandard2.0 - - - - - - - - - Semantic Kernel - Oobabooga Connector - Semantic Kernel connector for the oobabooga text-generation-webui open source project. Contains a client for text completion. - - - - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/README.md b/dotnet/src/Connectors/Connectors.AI.Oobabooga/README.md new file mode 100644 index 0000000000..b1c45cd528 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AI.Oobabooga/README.md @@ -0,0 +1,3 @@ +# Semantic Kernel Oobabooga AI Connector + +This connector have moved, please go [here](https://github.com/MyIntelligenceAgency/semantic-fleet) for more information. diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/OobaboogaTextCompletion.cs b/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/OobaboogaTextCompletion.cs deleted file mode 100644 index 6a0ee2174b..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/OobaboogaTextCompletion.cs +++ /dev/null @@ -1,481 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.WebSockets; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AI; -using Microsoft.SemanticKernel.AI.TextCompletion; -using Microsoft.SemanticKernel.Diagnostics; - -namespace Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -/// -/// Oobabooga text completion service API. -/// Adapted from -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class OobaboogaTextCompletion : ITextCompletion -{ - /// - /// The URI path for blocking API requests. - /// - public const string BlockingUriPath = "/api/v1/generate"; - private const string StreamingUriPath = "/api/v1/stream"; - - private readonly UriBuilder _blockingUri; - private readonly UriBuilder _streamingUri; - private readonly HttpClient _httpClient; - private readonly Func _webSocketFactory; - private readonly bool _useWebSocketsPooling; - private readonly int _maxNbConcurrentWebSockets; - private readonly SemaphoreSlim? _concurrentSemaphore; - private readonly ConcurrentBag? _activeConnections; - private readonly ConcurrentBag _webSocketPool = new(); - private readonly int _keepAliveWebSocketsDuration; - private readonly ILogger? _logger; - private Task? _cleanupTask = null; - private long _lastCallTicks = long.MaxValue; - - /// - /// Controls the size of the buffer used to receive websocket packets. - /// - public int WebSocketBufferSize { get; set; } = 2048; - - /// - /// Initializes a new instance of the class. - /// - /// The service API endpoint to which requests should be sent. - /// The port used for handling blocking requests. Default value is 5000 - /// The port used for handling streaming requests. Default value is 5005 - /// You can optionally set a hard limit on the max number of concurrent calls to the either of the completion methods by providing a . Calls in excess will wait for existing consumers to release the semaphore - /// Optional. The HTTP client used for making blocking API requests. If not specified, a default client will be used. - /// If true, websocket clients will be recycled in a reusable pool as long as concurrent calls are detected - /// if websocket pooling is enabled, you can provide an optional CancellationToken to properly dispose of the clean up tasks when disposing of the connector - /// When pooling is enabled, pooled websockets are flushed on a regular basis when no more connections are made. This is the time to keep them in pool before flushing - /// The WebSocket factory used for making streaming API requests. Note that only when pooling is enabled will websocket be recycled and reused for the specified duration. Otherwise, a new websocket is created for each call and closed and disposed afterwards, to prevent data corruption from concurrent calls. - /// Application logger - public OobaboogaTextCompletion(Uri endpoint, - int blockingPort = 5000, - int streamingPort = 5005, - SemaphoreSlim? concurrentSemaphore = null, - HttpClient? httpClient = null, - bool useWebSocketsPooling = true, - CancellationToken? webSocketsCleanUpCancellationToken = default, - int keepAliveWebSocketsDuration = 100, - Func? webSocketFactory = null, - ILogger? logger = null) - { - Verify.NotNull(endpoint); - this._blockingUri = new UriBuilder(endpoint) - { - Port = blockingPort, - Path = BlockingUriPath - }; - this._streamingUri = new(endpoint) - { - Port = streamingPort, - Path = StreamingUriPath - }; - if (this._streamingUri.Uri.Scheme.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - this._streamingUri.Scheme = (this._streamingUri.Scheme == "https") ? "wss" : "ws"; - } - - this._httpClient = httpClient ?? new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false); - this._useWebSocketsPooling = useWebSocketsPooling; - this._keepAliveWebSocketsDuration = keepAliveWebSocketsDuration; - this._logger = logger; - if (webSocketFactory != null) - { - this._webSocketFactory = () => - { - var webSocket = webSocketFactory(); - this.SetWebSocketOptions(webSocket); - return webSocket; - }; - } - else - { - this._webSocketFactory = () => - { - ClientWebSocket webSocket = new(); - this.SetWebSocketOptions(webSocket); - return webSocket; - }; - } - - // if a hard limit is defined, we use a semaphore to limit the number of concurrent calls, otherwise, we use a stack to track active connections - if (concurrentSemaphore != null) - { - this._concurrentSemaphore = concurrentSemaphore; - this._maxNbConcurrentWebSockets = concurrentSemaphore.CurrentCount; - } - else - { - this._activeConnections = new(); - this._maxNbConcurrentWebSockets = 0; - } - - if (this._useWebSocketsPooling) - { - this.StartCleanupTask(webSocketsCleanUpCancellationToken ?? CancellationToken.None); - } - } - - /// - public async IAsyncEnumerable GetStreamingCompletionsAsync( - string text, - AIRequestSettings? requestSettings = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await this.StartConcurrentCallAsync(cancellationToken).ConfigureAwait(false); - - var completionRequest = this.CreateOobaboogaRequest(text, requestSettings); - - var requestJson = JsonSerializer.Serialize(completionRequest); - - var requestBytes = Encoding.UTF8.GetBytes(requestJson); - - ClientWebSocket? clientWebSocket = null; - try - { - // if pooling is enabled, web socket is going to be recycled for reuse, if not it will be properly disposed of after the call -#pragma warning disable CA2000 // Dispose objects before losing scope - if (!this._useWebSocketsPooling || !this._webSocketPool.TryTake(out clientWebSocket)) - { - clientWebSocket = this._webSocketFactory(); - } -#pragma warning restore CA2000 // Dispose objects before losing scope - if (clientWebSocket.State == WebSocketState.None) - { - await clientWebSocket.ConnectAsync(this._streamingUri.Uri, cancellationToken).ConfigureAwait(false); - } - - var sendSegment = new ArraySegment(requestBytes); - await clientWebSocket.SendAsync(sendSegment, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); - - TextCompletionStreamingResult streamingResult = new(); - - var processingTask = this.ProcessWebSocketMessagesAsync(clientWebSocket, streamingResult, cancellationToken); - - yield return streamingResult; - - // Await the processing task to make sure it's finished before continuing - await processingTask.ConfigureAwait(false); - } - finally - { - if (clientWebSocket != null) - { - if (this._useWebSocketsPooling && clientWebSocket.State == WebSocketState.Open) - { - this._webSocketPool.Add(clientWebSocket); - } - else - { - await this.DisposeClientGracefullyAsync(clientWebSocket).ConfigureAwait(false); - } - } - - this.FinishConcurrentCall(); - } - } - - /// - public async Task> GetCompletionsAsync( - string text, - AIRequestSettings? requestSettings = null, - CancellationToken cancellationToken = default) - { - try - { - await this.StartConcurrentCallAsync(cancellationToken).ConfigureAwait(false); - - var completionRequest = this.CreateOobaboogaRequest(text, requestSettings); - - using var stringContent = new StringContent( - JsonSerializer.Serialize(completionRequest), - Encoding.UTF8, - "application/json"); - - using var httpRequestMessage = new HttpRequestMessage() - { - Method = HttpMethod.Post, - RequestUri = this._blockingUri.Uri, - Content = stringContent - }; - httpRequestMessage.Headers.Add("User-Agent", Telemetry.HttpUserAgent); - - using var response = await this._httpClient.SendWithSuccessCheckAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); - - var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - - TextCompletionResponse? completionResponse = JsonSerializer.Deserialize(body); - - if (completionResponse is null) - { - throw new SKException($"Unexpected response from Oobabooga API: {body}"); - } - - return completionResponse.Results.Select(completionText => new TextCompletionResult(completionText)).ToList(); - } - finally - { - this.FinishConcurrentCall(); - } - } - - #region private ================================================================================ - - /// - /// Creates an Oobabooga request, mapping dynamic request setting fields to their Oobabooga API counter parts - /// - /// The text to complete. - /// The request settings. - /// An Oobabooga TextCompletionRequest object with the text and completion parameters. - private TextCompletionRequest CreateOobaboogaRequest(string text, AIRequestSettings? requestSettings) - { - if (string.IsNullOrWhiteSpace(text)) - { - throw new ArgumentNullException(nameof(text)); - } - - if (requestSettings is null) - { - return new TextCompletionRequest() - { - Prompt = text - }; - } - - if (requestSettings is TextCompletionRequest requestSettingsTextCompletionRequest) - { - requestSettingsTextCompletionRequest.Prompt = text; - return requestSettingsTextCompletionRequest; - } - - var json = JsonSerializer.Serialize(requestSettings); - var textCompletionRequest = JsonSerializer.Deserialize(json); - - if (textCompletionRequest is not null) - { - textCompletionRequest.Prompt = text; - return textCompletionRequest; - } - - throw new ArgumentException("Invalid request settings, cannot convert to TextCompletionRequest", nameof(requestSettings)); - } - - /// - /// Sets the options for the , either persistent and provided by the ctor, or transient if none provided. - /// - private void SetWebSocketOptions(ClientWebSocket clientWebSocket) - { - clientWebSocket.Options.SetRequestHeader("User-Agent", Telemetry.HttpUserAgent); - } - - /// - /// That method is responsible for processing the websocket messages that build a streaming response object. It is crucial that it is run asynchronously to prevent a deadlock with results iteration - /// - private async Task ProcessWebSocketMessagesAsync(ClientWebSocket clientWebSocket, TextCompletionStreamingResult streamingResult, CancellationToken cancellationToken) - { - var buffer = new byte[this.WebSocketBufferSize]; - var finishedProcessing = false; - while (!finishedProcessing && !cancellationToken.IsCancellationRequested) - { - MemoryStream messageStream = new(); - WebSocketReceiveResult result; - do - { - var segment = new ArraySegment(buffer); - result = await clientWebSocket.ReceiveAsync(segment, cancellationToken).ConfigureAwait(false); - await messageStream.WriteAsync(buffer, 0, result.Count, cancellationToken).ConfigureAwait(false); - } while (!result.EndOfMessage); - - messageStream.Seek(0, SeekOrigin.Begin); - - if (result.MessageType == WebSocketMessageType.Text) - { - string messageText; - using (var reader = new StreamReader(messageStream, Encoding.UTF8)) - { - messageText = await reader.ReadToEndAsync().ConfigureAwait(false); - } - - var responseObject = JsonSerializer.Deserialize(messageText); - - if (responseObject is null) - { - throw new SKException($"Unexpected response from Oobabooga API: {messageText}"); - } - - switch (responseObject.Event) - { - case TextCompletionStreamingResponse.ResponseObjectTextStreamEvent: - streamingResult.AppendResponse(responseObject); - break; - case TextCompletionStreamingResponse.ResponseObjectStreamEndEvent: - streamingResult.SignalStreamEnd(); - if (!this._useWebSocketsPooling) - { - await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge stream-end oobabooga message", CancellationToken.None).ConfigureAwait(false); - } - - finishedProcessing = true; - break; - default: - break; - } - } - else if (result.MessageType == WebSocketMessageType.Close) - { - await clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None).ConfigureAwait(false); - finishedProcessing = true; - } - - if (clientWebSocket.State != WebSocketState.Open) - { - finishedProcessing = true; - } - } - } - - /// - /// Starts a concurrent call, either by taking a semaphore slot or by pushing a value on the active connections stack - /// - /// - private async Task StartConcurrentCallAsync(CancellationToken cancellationToken) - { - if (this._concurrentSemaphore != null) - { - await this._concurrentSemaphore!.WaitAsync(cancellationToken).ConfigureAwait(false); - } - else - { - this._activeConnections!.Add(true); - } - } - - /// - /// Gets the number of concurrent calls, either by reading the semaphore count or by reading the active connections stack count - /// - /// - private int GetCurrentConcurrentCallsNb() - { - if (this._concurrentSemaphore != null) - { - return this._maxNbConcurrentWebSockets - this._concurrentSemaphore!.CurrentCount; - } - - return this._activeConnections!.Count; - } - - /// - /// Ends a concurrent call, either by releasing a semaphore slot or by popping a value from the active connections stack - /// - private void FinishConcurrentCall() - { - if (this._concurrentSemaphore != null) - { - this._concurrentSemaphore!.Release(); - } - else - { - this._activeConnections!.TryTake(out _); - } - - Interlocked.Exchange(ref this._lastCallTicks, DateTime.UtcNow.Ticks); - } - - private void StartCleanupTask(CancellationToken cancellationToken) - { - if (this._cleanupTask == null || this._cleanupTask.IsCompleted) - { - this._cleanupTask = Task.Factory.StartNew( - async () => - { - while (!cancellationToken.IsCancellationRequested) - { - await this.FlushWebSocketClientsAsync(cancellationToken).ConfigureAwait(false); - } - }, - cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - } - } - - /// - /// Flushes the web socket clients that have been idle for too long - /// - /// - private async Task FlushWebSocketClientsAsync(CancellationToken cancellationToken) - { - // In the cleanup task, make sure you handle OperationCanceledException appropriately - // and make frequent checks on whether cancellation is requested. - try - { - if (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(this._keepAliveWebSocketsDuration, cancellationToken).ConfigureAwait(false); - - // If another call was made during the delay, do not proceed with flushing - if (DateTime.UtcNow.Ticks - Interlocked.Read(ref this._lastCallTicks) < TimeSpan.FromMilliseconds(this._keepAliveWebSocketsDuration).Ticks) - { - return; - } - - while (this.GetCurrentConcurrentCallsNb() == 0 && this._webSocketPool.TryTake(out ClientWebSocket clientToDispose)) - { - await this.DisposeClientGracefullyAsync(clientToDispose).ConfigureAwait(false); - } - } - } - catch (OperationCanceledException exception) - { - this._logger?.LogTrace(message: "FlushWebSocketClientsAsync cleaning task was cancelled", exception: exception); - while (this._webSocketPool.TryTake(out ClientWebSocket clientToDispose)) - { - await this.DisposeClientGracefullyAsync(clientToDispose).ConfigureAwait(false); - } - } - } - - /// - /// Closes and disposes of a client web socket after use - /// - private async Task DisposeClientGracefullyAsync(ClientWebSocket clientWebSocket) - { - try - { - if (clientWebSocket.State == WebSocketState.Open) - { - await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing client before disposal", CancellationToken.None).ConfigureAwait(false); - } - } - catch (OperationCanceledException exception) - { - this._logger?.LogTrace(message: "Closing client web socket before disposal was cancelled", exception: exception); - } - catch (WebSocketException exception) - { - this._logger?.LogTrace(message: "Closing client web socket before disposal raised web socket exception", exception: exception); - } - finally - { - clientWebSocket.Dispose(); - } - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionRequest.cs b/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionRequest.cs deleted file mode 100644 index 0b64735a46..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionRequest.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.AI; - -namespace Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -/// -/// HTTP schema to perform oobabooga completion request. Contains many parameters, some of which are specific to certain kinds of models. -/// See and subsequent links for additional information. -/// -[Serializable] -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class TextCompletionRequest : AIRequestSettings -{ - /// - /// The prompt text to complete. - /// - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; - - /// - /// The maximum number of tokens to generate, ignoring the number of tokens in the prompt. - /// - [JsonPropertyName("max_new_tokens")] - public int? MaxNewTokens { get; set; } - - /// - /// Determines whether or not to use sampling; use greedy decoding if false. - /// - [JsonPropertyName("do_sample")] - public bool DoSample { get; set; } = true; - - /// - /// Modulates the next token probabilities. A value of 0 implies deterministic output (only the most likely token is used). Higher values increase randomness. - /// - [JsonPropertyName("temperature")] - public double Temperature { get; set; } - - /// - /// If set to a value less than 1, only the most probable tokens with cumulative probability less than this value are kept for generation. - /// - [JsonPropertyName("top_p")] - public double TopP { get; set; } - - /// - /// Measures how similar the conditional probability of predicting a target token is to the expected conditional probability of predicting a random token, given the generated text. - /// - [JsonPropertyName("typical_p")] - public double TypicalP { get; set; } = 1; - - /// - /// Sets a probability floor below which tokens are excluded from being sampled. - /// - [JsonPropertyName("epsilon_cutoff")] - public double EpsilonCutoff { get; set; } - - /// - /// Used with top_p, top_k, and epsilon_cutoff set to 0. This parameter hybridizes locally typical sampling and epsilon sampling. - /// - [JsonPropertyName("eta_cutoff")] - public double EtaCutoff { get; set; } - - /// - /// Controls Tail Free Sampling (value between 0 and 1) - /// - [JsonPropertyName("tfs")] - public double Tfs { get; set; } = 1; - - /// - /// Top A Sampling is a way to pick the next word in a sentence based on how important it is in the context. Top-A considers the probability of the most likely token, and sets a limit based on its percentage. After this, remaining tokens are compared to this limit. If their probability is too low, they are removed from the pool​. - /// - [JsonPropertyName("top_a")] - public double TopA { get; set; } - - /// - /// Exponential penalty factor for repeating prior tokens. 1 means no penalty, higher value = less repetition. - /// - [JsonPropertyName("repetition_penalty")] - public double RepetitionPenalty { get; set; } = 1.18; - - /// - ///When using "top k", you select the top k most likely words to come next based on their probability of occurring, where k is a fixed number that you specify. You can use Top_K to control the amount of diversity in the model output​ - /// - [JsonPropertyName("top_k")] - public int TopK { get; set; } - - /// - /// Minimum length of the sequence to be generated. - /// - [JsonPropertyName("min_length")] - public int MinLength { get; set; } - - /// - /// If set to a value greater than 0, all ngrams of that size can only occur once. - /// - [JsonPropertyName("no_repeat_ngram_size")] - public int NoRepeatNgramSize { get; set; } - - /// - /// Number of beams for beam search. 1 means no beam search. - /// - [JsonPropertyName("num_beams")] - public int NumBeams { get; set; } = 1; - - /// - /// The values balance the model confidence and the degeneration penalty in contrastive search decoding. - /// - [JsonPropertyName("penalty_alpha")] - public int PenaltyAlpha { get; set; } - - /// - /// Exponential penalty to the length that is used with beam-based generation - /// - [JsonPropertyName("length_penalty")] - public double LengthPenalty { get; set; } = 1; - - /// - /// Controls the stopping condition for beam-based methods, like beam-search. It accepts the following values: True, where the generation stops as soon as there are num_beams complete candidates; False, where an heuristic is applied and the generation stops when is it very unlikely to find better candidates. - /// - [JsonPropertyName("early_stopping")] - public bool EarlyStopping { get; set; } - - /// - /// Parameter used for mirostat sampling in Llama.cpp, controlling perplexity during text (default: 0, 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0) - /// - [JsonPropertyName("mirostat_mode")] - public int MirostatMode { get; set; } - - /// - /// Set the Mirostat target entropy, parameter tau (default: 5.0) - /// - [JsonPropertyName("mirostat_tau")] - public int MirostatTau { get; set; } = 5; - - /// - /// Set the Mirostat learning rate, parameter eta (default: 0.1) - /// - [JsonPropertyName("mirostat_eta")] - public double MirostatEta { get; set; } = 0.1; - - /// - /// Random seed to control sampling, used when DoSample is True. - /// - [JsonPropertyName("seed")] - public int Seed { get; set; } = -1; - - /// - /// Controls whether to add beginning of a sentence token - /// - [JsonPropertyName("add_bos_token")] - public bool AddBosToken { get; set; } = true; - - /// - /// The leftmost tokens are removed if the prompt exceeds this length. Most models require this to be at most 2048. - /// - [JsonPropertyName("truncation_length")] - public int TruncationLength { get; set; } = 2048; - - /// - /// Forces the model to never end the generation prematurely. - /// - [JsonPropertyName("ban_eos_token")] - public bool BanEosToken { get; set; } = true; - - /// - /// Some specific models need this unset. - /// - [JsonPropertyName("skip_special_tokens")] - public bool SkipSpecialTokens { get; set; } = true; - - /// - /// In addition to the defaults. Written between "" and separated by commas. For instance: "\nYour Assistant:", "\nThe assistant:" - /// - [JsonPropertyName("stopping_strings")] - public IList StoppingStrings { get; set; } = new List(); -} diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResponse.cs b/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResponse.cs deleted file mode 100644 index ac3a60ad20..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResponse.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -/// -/// HTTP Schema for Oobabooga completion response. Contains a list of results. Adapted from -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class TextCompletionResponse -{ - /// - /// A field used by Oobabooga to return results from the blocking API. - /// - [JsonPropertyName("results")] - public List Results { get; set; } = new(); -} - -/// -/// HTTP Schema for an single Oobabooga result as part of a completion response. -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class TextCompletionResponseText -{ - /// - /// Completed text. - /// - [JsonPropertyName("text")] - public string? Text { get; set; } = string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResult.cs b/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResult.cs deleted file mode 100644 index 8961b758fa..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.AI.TextCompletion; -using Microsoft.SemanticKernel.Orchestration; - -namespace Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -/// -/// Oobabooga implementation of . Actual response object is stored in a ModelResult instance, and completion text is simply passed forward. -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -internal sealed class TextCompletionResult : ITextResult -{ - private readonly ModelResult _responseData; - - public TextCompletionResult(TextCompletionResponseText responseData) - { - this._responseData = new ModelResult(responseData); - } - - public ModelResult ModelResult => this._responseData; - - public Task GetCompletionAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(this._responseData.GetResult().Text ?? string.Empty); - } -} diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResponse.cs b/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResponse.cs deleted file mode 100644 index 3951b638e9..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResponse.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -/// -/// Represents the HTTP schema for streaming completion response. Adapted from . -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class TextCompletionStreamingResponse -{ - /// - /// Constant string representing the event that is fired when text is received from a websocket. - /// - public const string ResponseObjectTextStreamEvent = "text_stream"; - - /// - /// Constant string representing the event that is fired when streaming from a websocket ends. - /// - public const string ResponseObjectStreamEndEvent = "stream_end"; - - /// - /// A field used by Oobabooga to signal the type of websocket message sent, e.g. "text_stream" or "stream_end". - /// - [JsonPropertyName("event")] - public string Event { get; set; } = string.Empty; - - /// - /// A field used by Oobabooga to signal the number of messages sent, starting with 0 and incremented on each message. - /// - [JsonPropertyName("message_num")] - public int MessageNum { get; set; } - - /// - /// A field used by Oobabooga with the text chunk sent in the websocket message. - /// - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResult.cs b/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResult.cs deleted file mode 100644 index e780d45cd6..0000000000 --- a/dotnet/src/Connectors/Connectors.AI.Oobabooga/TextCompletion/TextCompletionStreamingResult.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.AI.TextCompletion; -using Microsoft.SemanticKernel.Orchestration; - -namespace Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -internal sealed class TextCompletionStreamingResult : ITextStreamingResult, ITextResult -{ - private readonly List _modelResponses; - private readonly Channel _responseChannel; - - public ModelResult ModelResult { get; } - - public TextCompletionStreamingResult() - { - this._modelResponses = new(); - this.ModelResult = new ModelResult(this._modelResponses); - this._responseChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() - { - SingleReader = true, - SingleWriter = true, - AllowSynchronousContinuations = false - }); - } - - public void AppendResponse(TextCompletionStreamingResponse response) - { - this._modelResponses.Add(response); - this._responseChannel.Writer.TryWrite(response.Text); - } - - public void SignalStreamEnd() - { - this._responseChannel.Writer.Complete(); - } - - public async Task GetCompletionAsync(CancellationToken cancellationToken = default) - { - StringBuilder resultBuilder = new(); - - await foreach (var chunk in this.GetCompletionStreamingAsync(cancellationToken)) - { - resultBuilder.Append(chunk); - } - - return resultBuilder.ToString(); - } - - public async IAsyncEnumerable GetCompletionStreamingAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - while (await this._responseChannel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) - { - while (this._responseChannel.Reader.TryRead(out string? chunk)) - { - yield return chunk; - } - } - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/ConnectedClient.cs b/dotnet/src/Connectors/Connectors.UnitTests/ConnectedClient.cs deleted file mode 100644 index b47c192dbd..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/ConnectedClient.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net; -using System.Net.WebSockets; - -namespace SemanticKernel.Connectors.UnitTests; - -internal sealed class ConnectedClient -{ - public Guid Id { get; } - public HttpListenerContext Context { get; } - public WebSocket? Socket { get; private set; } - - public ConnectedClient(Guid id, HttpListenerContext context) - { - this.Id = id; - this.Context = context; - } - - public void SetSocket(WebSocket socket) - { - this.Socket = socket; - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj index 7ec3d3599b..748c53e465 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj @@ -32,7 +32,6 @@ - @@ -59,12 +58,6 @@ Always - - Always - - - Always - diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaTestHelper.cs b/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaTestHelper.cs deleted file mode 100644 index 0df5eda9dd..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaTestHelper.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Moq; -using Moq.Protected; - -namespace SemanticKernel.Connectors.UnitTests.Oobabooga; - -/// -/// Helper for Oobabooga test purposes. -/// -internal static class OobaboogaTestHelper -{ - /// - /// Reads test response from file for mocking purposes. - /// - /// Name of the file with test response. - internal static string GetTestResponse(string fileName) - { - return File.ReadAllText($"./Oobabooga/TestData/{fileName}"); - } - - /// - /// Returns mocked instance of . - /// - /// Message to return for mocked . - internal static HttpClientHandler GetHttpClientHandlerMock(HttpResponseMessage httpResponseMessage) - { - var httpClientHandler = new Mock(); - - httpClientHandler - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(httpResponseMessage); - - return httpClientHandler.Object; - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaWebSocketTestServer.cs b/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaWebSocketTestServer.cs deleted file mode 100644 index 7a4608d456..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/OobaboogaWebSocketTestServer.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; - -namespace SemanticKernel.Connectors.UnitTests.Oobabooga; - -/// -/// Represents a WebSocket test server specifically designed for the Oobabooga text completion service. -/// It inherits from the base WebSocketTestServer class and handles Oobabooga-specific request and response classes. -/// The server accepts WebSocket connections, receives requests, and generates responses based on the Oobabooga text completion logic. -/// The OobaboogaWebSocketTestServer class uses a delegate to handle the request and response logic, allowing customization of the behavior. -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -internal sealed class OobaboogaWebSocketTestServer : WebSocketTestServer -{ - public OobaboogaWebSocketTestServer(string url, Func> stringHandler, ILogger? logger = null) - : base(url, bytes => HandleRequest(bytes, stringHandler), logger: logger) - { - } - - private static List> HandleRequest(ArraySegment request, Func> stringHandler) - { - var requestString = Encoding.UTF8.GetString(request.ToArray()); - var requestObj = JsonSerializer.Deserialize(requestString); - - var responseList = stringHandler(requestObj?.Prompt ?? string.Empty); - - var responseSegments = new List>(); - int messageNum = 0; - foreach (var responseChunk in responseList) - { - var responseObj = new TextCompletionStreamingResponse - { - Event = "text_stream", - MessageNum = messageNum, - Text = responseChunk - }; - - var responseJson = JsonSerializer.Serialize(responseObj); - var responseBytes = Encoding.UTF8.GetBytes(responseJson); - responseSegments.Add(new ArraySegment(responseBytes)); - - messageNum++; - } - - var streamEndObj = new TextCompletionStreamingResponse - { - Event = "stream_end", - MessageNum = messageNum - }; - - var streamEndJson = JsonSerializer.Serialize(streamEndObj); - var streamEndBytes = Encoding.UTF8.GetBytes(streamEndJson); - responseSegments.Add(new ArraySegment(streamEndBytes)); - - return responseSegments; - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_response.json deleted file mode 100644 index 397ee62436..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_response.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "results": [ - { - "text": "This is test completion response" - - } - ] - -} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_streaming_response.json b/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_streaming_response.json deleted file mode 100644 index bf731d3140..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TestData/completion_test_streaming_response.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "event": "text_stream", - "message_num": 0, - "text": "This is test completion response" -} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TextCompletion/OobaboogaTextCompletionTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TextCompletion/OobaboogaTextCompletionTests.cs deleted file mode 100644 index e9503a5915..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/Oobabooga/TextCompletion/OobaboogaTextCompletionTests.cs +++ /dev/null @@ -1,401 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Net.WebSockets; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AI.TextCompletion; -using Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.Connectors.UnitTests.Oobabooga.TextCompletion; - -/// -/// Unit tests for class. -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class OobaboogaTextCompletionTests : IDisposable -{ - private readonly XunitLogger _logger; - private const string EndPoint = "https://fake-random-test-host"; - private const int BlockingPort = 1234; - private const int StreamingPort = 2345; - private const string CompletionText = "fake-test"; - private const string CompletionMultiText = "Hello, my name is"; - - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Uri _endPointUri; - private readonly string _streamCompletionResponseStub; - - public OobaboogaTextCompletionTests(ITestOutputHelper output) - { - this._logger = new XunitLogger(output); - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._messageHandlerStub.ResponseToReturn.Content = new StringContent(OobaboogaTestHelper.GetTestResponse("completion_test_response.json")); - this._streamCompletionResponseStub = OobaboogaTestHelper.GetTestResponse("completion_test_streaming_response.json"); - - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._endPointUri = new Uri(EndPoint); - } - - [Fact] - public async Task UserAgentHeaderShouldBeUsedAsync() - { - //Arrange - var sut = new OobaboogaTextCompletion(endpoint: this._endPointUri, - blockingPort: BlockingPort, - httpClient: this._httpClient, - logger: this._logger); - - //Act - await sut.GetCompletionsAsync(CompletionText, null); - - //Assert - Assert.True(this._messageHandlerStub.RequestHeaders?.Contains("User-Agent")); - - var values = this._messageHandlerStub.RequestHeaders!.GetValues("User-Agent"); - - var value = values.SingleOrDefault(); - Assert.Equal("Semantic-Kernel", value); - } - - [Fact] - public async Task ProvidedEndpointShouldBeUsedAsync() - { - //Arrange - var sut = new OobaboogaTextCompletion(endpoint: this._endPointUri, - blockingPort: BlockingPort, - httpClient: this._httpClient, - logger: this._logger); - - //Act - await sut.GetCompletionsAsync(CompletionText, null); - - //Assert - Assert.StartsWith(EndPoint, this._messageHandlerStub.RequestUri?.AbsoluteUri, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task BlockingUrlShouldBeBuiltSuccessfullyAsync() - { - //Arrange - var sut = new OobaboogaTextCompletion(endpoint: this._endPointUri, - blockingPort: BlockingPort, - httpClient: this._httpClient, - logger: this._logger); - - //Act - await sut.GetCompletionsAsync(CompletionText); - var expectedUri = new UriBuilder(this._endPointUri) - { - Path = OobaboogaTextCompletion.BlockingUriPath, - Port = BlockingPort - }; - - //Assert - Assert.Equal(expectedUri.Uri, this._messageHandlerStub.RequestUri); - } - - [Fact] - public async Task ShouldSendPromptToServiceAsync() - { - //Arrange - var sut = new OobaboogaTextCompletion(endpoint: this._endPointUri, - blockingPort: BlockingPort, - httpClient: this._httpClient, - logger: this._logger); - - //Act - await sut.GetCompletionsAsync(CompletionText, null); - - //Assert - var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); - Assert.NotNull(requestPayload); - - Assert.Equal(CompletionText, requestPayload.Prompt); - } - - [Fact] - public async Task ShouldHandleServiceResponseAsync() - { - //Arrange - var sut = new OobaboogaTextCompletion(endpoint: this._endPointUri, - blockingPort: BlockingPort, - httpClient: this._httpClient, - logger: this._logger); - - //Act - var result = await sut.GetCompletionsAsync(CompletionText, null); - - //Assert - Assert.NotNull(result); - - var completions = result.SingleOrDefault(); - Assert.NotNull(completions); - - var completion = await completions.GetCompletionAsync(); - Assert.Equal("This is test completion response", completion); - } - - [Fact] - public async Task ShouldHandleStreamingServicePersistentWebSocketResponseAsync() - { - var requestMessage = CompletionText; - var expectedResponse = new List { this._streamCompletionResponseStub }; - await this.RunWebSocketMultiPacketStreamingTestAsync( - requestMessage: requestMessage, - expectedResponse: expectedResponse, - isPersistent: true); - } - - [Fact] - public async Task ShouldHandleStreamingServiceTransientWebSocketResponseAsync() - { - var requestMessage = CompletionText; - var expectedResponse = new List { this._streamCompletionResponseStub }; - await this.RunWebSocketMultiPacketStreamingTestAsync( - requestMessage: requestMessage, - expectedResponse: expectedResponse); - } - - [Fact] - public async Task ShouldHandleConcurrentWebSocketConnectionsAsync() - { - var serverUrl = $"http://localhost:{StreamingPort}/"; - var clientUrl = $"ws://localhost:{StreamingPort}/"; - var expectedResponses = new List - { - "Response 1", - "Response 2", - "Response 3", - "Response 4", - "Response 5" - }; - - await using var server = new WebSocketTestServer(serverUrl, request => - { - // Simulate different responses for each request - var responseIndex = int.Parse(Encoding.UTF8.GetString(request.ToArray()), CultureInfo.InvariantCulture); - byte[] bytes = Encoding.UTF8.GetBytes(expectedResponses[responseIndex]); - var toReturn = new List> { new(bytes) }; - return toReturn; - }); - - var tasks = new List>(); - - // Simulate multiple concurrent WebSocket connections - for (int i = 0; i < expectedResponses.Count; i++) - { - var currentIndex = i; - tasks.Add(Task.Run(async () => - { - using var client = new ClientWebSocket(); - await client.ConnectAsync(new Uri(clientUrl), CancellationToken.None); - - // Send a request to the server - var requestBytes = Encoding.UTF8.GetBytes(currentIndex.ToString(CultureInfo.InvariantCulture)); - await client.SendAsync(new ArraySegment(requestBytes), WebSocketMessageType.Text, true, CancellationToken.None); - - // Receive the response from the server - var responseBytes = new byte[1024]; - var responseResult = await client.ReceiveAsync(new ArraySegment(responseBytes), CancellationToken.None); - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close connection after message received", CancellationToken.None); - - var response = Encoding.UTF8.GetString(responseBytes, 0, responseResult.Count); - - return response; - })); - } - - // Assert - for (int i = 0; i < expectedResponses.Count; i++) - { - var response = await tasks[i]; - Assert.Equal(expectedResponses[i], response); - } - } - - [Fact] - public async Task ShouldHandleMultiPacketStreamingServiceTransientWebSocketResponseAsync() - { - await this.RunWebSocketMultiPacketStreamingTestAsync(); - } - - [Fact] - public async Task ShouldHandleMultiPacketStreamingServicePersistentWebSocketResponseBroadcastBlockAsync() - { - await this.RunWebSocketMultiPacketStreamingTestAsync(isPersistent: true); - } - - [Fact] - public async Task ShouldHandleConcurrentMultiPacketStreamingServiceTransientWebSocketResponseAsync() - { - await this.RunWebSocketMultiPacketStreamingTestAsync(nbConcurrentCalls: 10); - } - - [Fact] - public async Task ShouldHandleConcurrentMultiPacketStreamingServicePersistentWebSocketResponseAsync() - { - await this.RunWebSocketMultiPacketStreamingTestAsync(nbConcurrentCalls: 10, isPersistent: true); - } - - /// - /// This test will assess concurrent enumeration of the same long multi message (500 websocket messages) streaming result. - /// - [Fact] - public async Task ShouldHandleConcurrentEnumerationOfLongStreamingServiceResponseAsync() - { - var expectedResponse = Enumerable.Range(0, 500).Select(i => i.ToString(CultureInfo.InvariantCulture)).ToList(); - using SemaphoreSlim enforcedConcurrentCallSemaphore = new(20); - await this.RunWebSocketMultiPacketStreamingTestAsync( - expectedResponse: expectedResponse, - nbConcurrentCalls: 1, - nbConcurrentEnumeration: 100, - isPersistent: true, - keepAliveWebSocketsDuration: 100, - concurrentCallsTicksDelay: 0, - enforcedConcurrentCallSemaphore: enforcedConcurrentCallSemaphore, - maxExpectedNbClients: 20); - } - - private async Task RunWebSocketMultiPacketStreamingTestAsync( - string requestMessage = CompletionMultiText, - List? expectedResponse = null, - int nbConcurrentCalls = 1, - int nbConcurrentEnumeration = 1, - bool isPersistent = false, - int requestProcessingDuration = 0, - int segmentMessageDelay = 0, - int keepAliveWebSocketsDuration = 100, - int concurrentCallsTicksDelay = 0, - SemaphoreSlim? enforcedConcurrentCallSemaphore = null, - int maxExpectedNbClients = 0, - int maxTestDuration = 0) - { - if (expectedResponse == null) - { - expectedResponse = new List { " John", ". I", "'m a", " writer" }; - } - - Func? webSocketFactory = null; - // Counter to track the number of WebSocket clients created - int clientCount = 0; - var delayTimeSpan = new TimeSpan(concurrentCallsTicksDelay); - if (isPersistent) - { - ClientWebSocket ExternalWebSocketFactory() - { - this._logger?.LogInformation(message: "Creating new client web socket"); - var toReturn = new ClientWebSocket(); - return toReturn; - } - - if (maxExpectedNbClients > 0) - { - ClientWebSocket IncrementFactory() - { - var toReturn = ExternalWebSocketFactory(); - Interlocked.Increment(ref clientCount); - return toReturn; - } - - webSocketFactory = IncrementFactory; - } - else - { - webSocketFactory = ExternalWebSocketFactory; - } - } - - using var cleanupToken = new CancellationTokenSource(); - - var sut = new OobaboogaTextCompletion( - endpoint: new Uri("http://localhost/"), - streamingPort: StreamingPort, - httpClient: this._httpClient, - webSocketsCleanUpCancellationToken: cleanupToken.Token, - webSocketFactory: webSocketFactory, - keepAliveWebSocketsDuration: keepAliveWebSocketsDuration, - concurrentSemaphore: enforcedConcurrentCallSemaphore, - logger: this._logger); - - await using var server = new OobaboogaWebSocketTestServer($"http://localhost:{StreamingPort}/", request => expectedResponse, logger: this._logger) - { - RequestProcessingDelay = TimeSpan.FromMilliseconds(requestProcessingDuration), - SegmentMessageDelay = TimeSpan.FromMilliseconds(segmentMessageDelay) - }; - - var sw = Stopwatch.StartNew(); - var tasks = new List>>(); - - for (int i = 0; i < nbConcurrentCalls; i++) - { - tasks.Add(Task.FromResult(sut.CompleteStreamAsync(requestMessage, new TextCompletionRequest() - { - Temperature = 0.01, - MaxNewTokens = 7, - TopP = 0.1, - }, cancellationToken: cleanupToken.Token))); - } - - var callEnumerationTasks = new List>>(); - var results = await Task.WhenAll(tasks); - - foreach (var completion in results) - { - callEnumerationTasks.AddRange(Enumerable.Range(0, nbConcurrentEnumeration).Select(_ => Task.Run(async () => - { - var result = new List(); - await foreach (var chunk in completion) - { - result.Add(chunk); - } - - return result; - }))); - - // Introduce a delay between creating each WebSocket client - await Task.Delay(delayTimeSpan); - } - - var allResults = await Task.WhenAll(callEnumerationTasks); - - var elapsed = sw.ElapsedMilliseconds; - if (maxExpectedNbClients > 0) - { - Assert.InRange(clientCount, 1, maxExpectedNbClients); - } - - // Validate all results - foreach (var result in allResults) - { - Assert.Equal(expectedResponse.Count, result.Count); - for (int i = 0; i < expectedResponse.Count; i++) - { - Assert.Equal(expectedResponse[i], result[i]); - } - } - - if (maxTestDuration > 0) - { - Assert.InRange(elapsed, 0, maxTestDuration); - } - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - this._logger.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/WebSocketTestServer.cs b/dotnet/src/Connectors/Connectors.UnitTests/WebSocketTestServer.cs deleted file mode 100644 index c833797f32..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/WebSocketTestServer.cs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace SemanticKernel.Connectors.UnitTests; - -internal class WebSocketTestServer : IDisposable -{ - private readonly ILogger? _logger; - - private readonly HttpListener _httpListener; - private readonly CancellationTokenSource _mainCancellationTokenSource; - private readonly CancellationTokenSource _socketCancellationTokenSource; - private bool _serverIsRunning; - - private readonly Func, List>> _arraySegmentHandler; - private readonly ConcurrentDictionary> _requestContentQueues; - private readonly ConcurrentBag _runningTasks = new(); - - private readonly ConcurrentDictionary _clients = new(); - - private readonly Task? _handleRequestTask = null; - - public TimeSpan RequestProcessingDelay { get; set; } = TimeSpan.Zero; - public TimeSpan SegmentMessageDelay { get; set; } = TimeSpan.Zero; - - public ConcurrentDictionary RequestContents - { - get - { - return new ConcurrentDictionary( - this._requestContentQueues - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList().SelectMany(bytes => bytes).ToArray())); - } - } - - public WebSocketTestServer(string url, Func, List>> arraySegmentHandler, ILogger? logger = null) - { - this._logger = logger; - - this._arraySegmentHandler = arraySegmentHandler; - this._requestContentQueues = new ConcurrentDictionary>(); - - this._mainCancellationTokenSource = new(); - this._socketCancellationTokenSource = new(); - - this._httpListener = new HttpListener(); - this._httpListener.Prefixes.Add(url); - this._httpListener.Start(); - this._serverIsRunning = true; - - if (this._handleRequestTask is null || this._handleRequestTask.IsCompleted) - { - this._handleRequestTask = Task.Run((Func)this.HandleRequestsAsync, this._mainCancellationTokenSource.Token); - } - } - - private async Task HandleRequestsAsync() - { - while (!this._mainCancellationTokenSource.IsCancellationRequested) - { - var context = await this._httpListener.GetContextAsync().ConfigureAwait(false); - - if (this._serverIsRunning) - { - if (context.Request.IsWebSocketRequest) - { - var connectedClient = new ConnectedClient(Guid.NewGuid(), context); - this._clients[connectedClient.Id] = connectedClient; - try - { - var socketContext = await context.AcceptWebSocketAsync(subProtocol: null); - connectedClient.SetSocket(socketContext.WebSocket); - this._runningTasks.Add(this.HandleSingleWebSocketRequestAsync(connectedClient)); - } - catch - { - // server error if upgrade from HTTP to WebSocket fails - context.Response.StatusCode = 500; - context.Response.StatusDescription = "WebSocket upgrade failed"; - context.Response.Close(); - throw; - } - } - } - else - { - // HTTP 409 Conflict (with server's current state) - context.Response.StatusCode = 409; - context.Response.StatusDescription = "Server is shutting down"; - context.Response.Close(); - return; - } - } - - await Task.WhenAll(this._runningTasks).ConfigureAwait(false); - } - - private async Task HandleSingleWebSocketRequestAsync(ConnectedClient connectedClient) - { - var buffer = WebSocket.CreateServerBuffer(4096); - - Guid requestId = connectedClient.Id; - this._requestContentQueues[requestId] = new ConcurrentQueue(); - - try - { - while (!this._socketCancellationTokenSource.IsCancellationRequested && connectedClient.Socket != null && connectedClient.Socket.State != WebSocketState.Closed && connectedClient.Socket.State != WebSocketState.Aborted) - { - WebSocketReceiveResult result = await connectedClient.Socket.ReceiveAsync(buffer, this._socketCancellationTokenSource.Token).ConfigureAwait(false); - if (!this._socketCancellationTokenSource.IsCancellationRequested && connectedClient.Socket.State != WebSocketState.Closed && connectedClient.Socket.State != WebSocketState.Aborted) - { - if (connectedClient.Socket.State == WebSocketState.CloseReceived && result.MessageType == WebSocketMessageType.Close) - { - await connectedClient.Socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None); - - break; - } - - var receivedBytes = buffer.Slice(0, result.Count); - this._requestContentQueues[requestId].Enqueue(receivedBytes.ToArray()); - - if (result.EndOfMessage) - { - var responseSegments = this._arraySegmentHandler(receivedBytes); - - if (this.RequestProcessingDelay.Ticks > 0) - { - await Task.Delay(this.RequestProcessingDelay).ConfigureAwait(false); - } - - foreach (var responseSegment in responseSegments) - { - if (connectedClient.Socket.State != WebSocketState.Open) - { - break; - } - - if (this.SegmentMessageDelay.Ticks > 0) - { - await Task.Delay(this.SegmentMessageDelay).ConfigureAwait(false); - } - - await connectedClient.Socket.SendAsync(responseSegment, WebSocketMessageType.Text, true, this._socketCancellationTokenSource.Token).ConfigureAwait(false); - } - } - } - } - - if (connectedClient.Socket?.State == WebSocketState.Open) - { - await connectedClient.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing waiting for acknowledgement", CancellationToken.None).ConfigureAwait(false); - } - else if (connectedClient.Socket?.State == WebSocketState.CloseReceived) - { - await connectedClient.Socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing without waiting for acknowledgment", CancellationToken.None).ConfigureAwait(false); - } - } - catch (OperationCanceledException exception) - { - this._logger?.LogTrace(message: "Closing server web socket before disposal was cancelled", exception: exception); - } - catch (WebSocketException exception) - { - this._logger?.LogTrace(message: "Closing server web socket before disposal raised web socket exception", exception: exception); - } - finally - { - if (connectedClient.Socket?.State != WebSocketState.Closed) - { - connectedClient.Socket?.Abort(); - } - - connectedClient.Socket?.Dispose(); - - // Remove client from dictionary when done - this._clients.TryRemove(requestId, out _); - } - } - - private async Task CloseAllSocketsAsync() - { - // Close all active sockets before disposing - foreach (var client in this._clients.Values) - { - if (client.Socket?.State == WebSocketState.Open) - { - await client.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", this._mainCancellationTokenSource.Token); - } - } - } - - public async ValueTask DisposeAsync() - { - try - { - this._serverIsRunning = false; - await this.CloseAllSocketsAsync(); // Close all sockets before finishing the tasks - await Task.WhenAll(this._runningTasks).ConfigureAwait(false); - this._socketCancellationTokenSource.Cancel(); - this._mainCancellationTokenSource.Cancel(); - } - catch (OperationCanceledException exception) - { - this._logger?.LogTrace(message: "\"Disposing web socket test server raised operation cancel exception", exception: exception); - } - finally - { - this._httpListener.Stop(); - this._httpListener.Close(); - this._socketCancellationTokenSource.Dispose(); - this._mainCancellationTokenSource.Dispose(); - } - } - -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - public void Dispose() - { - this.DisposeAsync().AsTask().GetAwaiter().GetResult(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/XunitLogger.cs b/dotnet/src/Connectors/Connectors.UnitTests/XunitLogger.cs deleted file mode 100644 index 6a6ad5c22b..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/XunitLogger.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Extensions.Logging; -using Xunit.Abstractions; - -namespace SemanticKernel.Connectors.UnitTests; - -/// -/// A logger that writes to the Xunit test output -/// -internal sealed class XunitLogger : ILogger, IDisposable -{ - private readonly ITestOutputHelper _output; - - public XunitLogger(ITestOutputHelper output) - { - this._output = output; - } - - /// - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - this._output.WriteLine(state?.ToString()); - } - - /// - public bool IsEnabled(LogLevel logLevel) => true; - - /// - public IDisposable BeginScope(TState state) - => this; - - /// - public void Dispose() - { - // This class is marked as disposable to support the BeginScope method. - // However, there is no need to dispose anything. - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/Oobabooga/OobaboogaTextCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Oobabooga/OobaboogaTextCompletionTests.cs deleted file mode 100644 index b094f4e449..0000000000 --- a/dotnet/src/IntegrationTests/Connectors/Oobabooga/OobaboogaTextCompletionTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel.AI.TextCompletion; -using Microsoft.SemanticKernel.Connectors.AI.Oobabooga.TextCompletion; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.Oobabooga; - -/// -/// Integration tests for . -/// -[Obsolete("This functionality is available as part of new NuGet package: https://www.nuget.org/packages/MyIA.SemanticKernel.Connectors.AI.Oobabooga/. This will be removed in a future release.")] -public sealed class OobaboogaTextCompletionTests : IDisposable -{ - private const string Endpoint = "http://localhost"; - private const int BlockingPort = 5000; - private const int StreamingPort = 5005; - - private readonly IConfigurationRoot _configuration; - private readonly List _webSockets = new(); - private readonly Func _webSocketFactory; - - public OobaboogaTextCompletionTests() - { - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .Build(); - this._webSocketFactory = () => - { - var toReturn = new ClientWebSocket(); - this._webSockets.Add(toReturn); - return toReturn; - }; - } - - private const string Input = " My name is"; - - [Fact(Skip = "This test is for manual verification.")] - public async Task OobaboogaLocalTextCompletionAsync() - { - var oobaboogaLocal = new OobaboogaTextCompletion( - endpoint: new Uri(Endpoint), - blockingPort: BlockingPort); - - // Act - var localResponse = await oobaboogaLocal.CompleteAsync(Input, requestSettings: new TextCompletionRequest() - { - Temperature = 0.01, - MaxNewTokens = 7, - TopP = 0.1, - }); - - AssertAcceptableResponse(localResponse); - } - - [Fact(Skip = "This test is for manual verification.")] - public async Task OobaboogaLocalTextCompletionStreamingAsync() - { - var oobaboogaLocal = new OobaboogaTextCompletion( - endpoint: new Uri(Endpoint), - streamingPort: StreamingPort, - webSocketFactory: this._webSocketFactory); - - // Act - var localResponse = oobaboogaLocal.CompleteStreamAsync(Input, requestSettings: new TextCompletionRequest() - { - Temperature = 0.01, - MaxNewTokens = 7, - TopP = 0.1, - }); - - StringBuilder stringBuilder = new(); - await foreach (var result in localResponse) - { - stringBuilder.Append(result); - } - - var resultsMerged = stringBuilder.ToString(); - AssertAcceptableResponse(resultsMerged); - } - - private static void AssertAcceptableResponse(string localResponse) - { - // Assert - Assert.NotNull(localResponse); - // Depends on the target LLM obviously, but most LLMs should propose an arbitrary surname preceded by a white space, including the start prompt or not - // ie " My name is" => " John (...)" or " My name is" => " My name is John (...)". - // Here are a couple LLMs that were tested successfully: gpt2, aisquared_dlite-v1-355m, bigscience_bloomz-560m, eachadea_vicuna-7b-1.1, TheBloke_WizardLM-30B-GPTQ etc. - // A few will return an empty string, but well those shouldn't be used for integration tests. - var expectedRegex = new Regex(@"\s\w+.*"); - Assert.Matches(expectedRegex, localResponse); - } - - public void Dispose() - { - foreach (ClientWebSocket clientWebSocket in this._webSockets) - { - clientWebSocket.Dispose(); - } - } -} diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index cce2ae4513..7b2e9a85a2 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -31,7 +31,6 @@ - From 746b421e2f3daa69f905ef0bf75579ce0eeb58a5 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:42:19 +0000 Subject: [PATCH 21/24] .Net: Reduce visibility of two fields from public to private (#3354) ### Motivation and Context Two properties were inadvertently made public and should be private ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs b/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs index 8c14419c17..3f28915cb6 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/SemanticFunction.cs @@ -144,9 +144,9 @@ internal sealed class SemanticFunction : ISKFunction, IDisposable private static readonly JsonSerializerOptions s_toStringIndentedSerialization = new() { WriteIndented = true }; private readonly ILogger _logger; private IAIServiceSelector? _serviceSelector; - public List? _modelSettings; + private List? _modelSettings; private readonly Lazy _view; - public IPromptTemplate _promptTemplate { get; } + private readonly IPromptTemplate _promptTemplate; private static async Task GetCompletionsResultContentAsync(IReadOnlyList completions, CancellationToken cancellationToken = default) { From 054b01c1e74288b59ddeb639badbdff4b49bdbd7 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:21:55 +0000 Subject: [PATCH 22/24] ADR Update Kernel Hooks (aka Function Handlers) - Phase 1 (#3332) ### Motivation and Context Updated decision documentation. --- .../0001-madr-architecture-decisions.md | 34 +-- .../0005-function-execution-handlers.md | 134 ---------- docs/decisions/0005-kernel-hooks-phase1.md | 244 ++++++++++++++++++ .../0012-kernel-service-registration.md | 2 +- .../0014-chat-completion-roles-in-prompt.md | 2 +- 5 files changed, 264 insertions(+), 152 deletions(-) delete mode 100644 docs/decisions/0005-function-execution-handlers.md create mode 100644 docs/decisions/0005-kernel-hooks-phase1.md diff --git a/docs/decisions/0001-madr-architecture-decisions.md b/docs/decisions/0001-madr-architecture-decisions.md index 44008d442b..679d41b20d 100644 --- a/docs/decisions/0001-madr-architecture-decisions.md +++ b/docs/decisions/0001-madr-architecture-decisions.md @@ -2,10 +2,11 @@ # These are optional elements. Feel free to remove any of them. status: accepted date: 2023-05-29 -deciders: dluc,shawncal,hathind,alliscode -consulted: -informed: +deciders: dluc, shawncal, hathind, alliscode +consulted: +informed: --- + # Use Markdown Any Decision Records to track Semantic Kernel Architecture Decisions ## Context and Problem Statement @@ -18,14 +19,15 @@ MADR is a lean template to capture any decisions in a structured way. The templa For more information [see](https://adr.github.io/) + ## Decision Drivers -* Architecture changes and the associated decision making process should be transparent to the community. -* Decision records are stored in the repository and are easily discoverable for teams involved in the various language ports. +- Architecture changes and the associated decision making process should be transparent to the community. +- Decision records are stored in the repository and are easily discoverable for teams involved in the various language ports. ## Considered Options -* Use MADR format and store decision documents in the repository. +- Use MADR format and store decision documents in the repository. ## Decision Outcome @@ -38,21 +40,21 @@ Chosen option: How would we use ADR's to track technical decisions? 1. Copy docs/decisions/adr-template.md to docs/decisions/NNNN-title-with-dashes.md, where NNNN indicates the next number in sequence. - 1. Check for existing PR's to make sure you use the correct sequence number. - 2. There is also a short form template docs/decisions/adr-short-template.md + 1. Check for existing PR's to make sure you use the correct sequence number. + 2. There is also a short form template docs/decisions/adr-short-template.md 2. Edit NNNN-title-with-dashes.md. - 1. Status must initially be `proposed` - 2. List of `deciders` must include the aliases of the people who will sign off on the decision. - 3. The relevant EM and `dluc` must be listed as deciders or informed of all decisions. - 4. You should list the aliases of all partners who were consulted as part of the decision. + 1. Status must initially be `proposed` + 2. List of `deciders` must include the aliases of the people who will sign off on the decision. + 3. The relevant EM and `dluc` must be listed as deciders or informed of all decisions. + 4. You should list the aliases of all partners who were consulted as part of the decision. 3. For each option list the good, neutral and bad aspects of each considered alternative. - 1. Detailed investigations can be included in the `More Information` section inline or as links to external documents. + 1. Detailed investigations can be included in the `More Information` section inline or as links to external documents. 4. Share your PR with the deciders and other interested parties. 1. Deciders must be listed as required reviewers. 2. The status must be updated to `accepted` once a decision is agreed and the date must also be updated. 3. Approval of the decision is captured using PR approval. 5. Decisions can be changed later and superseded by a new ADR. In this case it is useful to record any negative outcomes in the original ADR. -* Good, because lightweight format which is easy to edit -* Good, because this uses the standard Git review process for commenting and approval -* Good, because decisions and review process are transparent to the community +- Good, because lightweight format which is easy to edit +- Good, because this uses the standard Git review process for commenting and approval +- Good, because decisions and review process are transparent to the community diff --git a/docs/decisions/0005-function-execution-handlers.md b/docs/decisions/0005-function-execution-handlers.md deleted file mode 100644 index ffccf538f5..0000000000 --- a/docs/decisions/0005-function-execution-handlers.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -# These are optional elements. Feel free to remove any of them. -status: accepted -contact: rogerbarreto -date: 2023-05-29 -deciders: @rogerbarreto, @shawncal, @stephentoub -consulted: -informed: ---- - -# Kernel/Function Handlers - -## Context and Problem Statement - -A Kernel function caller needs to be able to handle/intercept any function execution in the Kernel before and after it was attempted. Allowing it to modify the prompt, abort the execution, or modify the output and many other scenarios as follows: - -- Pre-Execution / Running - - - Get: Original prompt template - - Get: Prompt generated by the current Kernel `TemplateEngine` before calling the LLM - - Get: Current settings used - - Get: Parameters used - - Get: SKContext - - Set: Modify a prompt content before sending it to LLM - - Set: Abort/Cancel function execution - -- In-Execution / Stream Processing - - - Get: Text Prompt generated per stream block `IAsyncEnumerable` - - Set: Filter/Change a prompt stream block - - Set: Skip a prompt stream block - -- Post-Execution / Ran - - - Get: LLM Model Result (Tokens Usage, Stop Sequence, ...) - - Get: Generated Prompt - - Get: SKContext - - Get: Output parameters - - - Set: Modify a prompt content after getting it from LLM - - Set: Modify output parameters content (before returning the output) - -## Decision Drivers - -- Architecture changes and the associated decision making process should be transparent to the community. -- Decision records are stored in the repository and are easily discoverable for teams involved in the various language ports. - -## Considered Options - -- Callback Registration + Recursive -- Single Callback -- Event Based Registration -- Middleware - -## Pros and Cons of the Options - -### Callback Registration Recursive Delegate (Kernel, Plan, Function) - -- Specified on plan and function level as a configuration be able to specify what are the callback Handlers that will be triggered. - -Pros: - -- Common pattern for observing and also changing data exposed as parameter into the delegate signature for (Get/Set) scenarios -- Registering a callback gives back the registration object that can be used to cancel the execution of the function in the future. -- Recursive approach, allows to register multiple callbacks for the same event, and also allows to register callbacks on top of pre existing callbacks. - -Cons: - -- Registrations may use more memory and might not be garbage collected in the recursive approach, only when the function or the plan is disposed. - -### Single Callback Delegate (Kernel, Plan, Function) - -- Specified on kernel level as a configuration be able to specify what are the callback Handlers that will be triggered. - - Specified on function creation: As part of the function constructor be able to specify what are the callback Handlers that will be triggered. - - Specified on function invocation: As part of the function invoke be able to specify what are the callback Handlers as a parameter that will be triggered. - -Pros: - -- Common pattern for observing and also changing data exposed as parameter into the delegate signature for (Get/Set) scenarios - -Cons: - -- Limited to only one method observing a specific event (Pre Post and InExecution). - Function When used as parameter, three new parameters would be needed as part of the function. (Specified on function invocation) - Extra Cons on - -### Event Base Registration (Kernel only) - -Expose events on both IKernel and ISKFunction that the call can can be observing to interact. - -Pros: - -- Multiple Listeners can registered for the same event -- Listeners can be registered and unregistered at will -- Common pattern (EventArgs) for observing and also changing data exposed as parameter into the event signature for (Get/Set) scenarios - -Cons: - -- Event handlers are void, making the EventArgs by reference the only way to modify the data. -- Not clear how supportive is this approach for asynchronous pattern/multi threading -- Won't support `ISKFunction.InvokeAsync` - -### Middleware (Kernel Only) - -Specified on Kernel level, and would only be used using IKernel.RunAsync operation, this pattern would be similar to asp.net core middlewares, running the pipelines with a context and a requestdelegate next for controlling (Pre/Post conditions) - -Pros: - -- Common pattern for handling Pre/Post Setting/Filtering data - -Cons: - -- Functions can run on their own instance, middlewares suggest more complexity and the existence of an external container/manager (Kernel) to intercept/observe function calls. - -## Main Questions - -- Q: Post Execution Handlers should execute right after the LLM result or before the end of the function execution itself? - A: Currently post execution Handlers are executed after function execution. - -- Q: Should Pre/Post Handlers be many (pub/sub) allowing registration/deregistration? - A: By using the standard .NET event implementation, this already supports multiple registrations as well as deregistrations managed by the caller. - -- Q: Setting Handlers on top of pre existing Handlers should be allowed or throw an error? - A: By using the standard .NET event implementation, the stander behavior will not throw an error and will execute all the registered handlers. - -- Q: Setting Handlers on Plans should automatically cascade this Handlers for all the inner steps + overriding existing ones in the process? - A: Handlers will be triggered before and after each step is executed the same way the Kernel RunAsync pipeline works. - -- Q: When a pre execution handler cancel the execution of the function, - -- Q: When a pre function execution handler intents to cancel the execution, should further handlers in the chain be called or not? - A: Currently the standard .net behavior is to call all the registered handlers. This way function execution will solely depends on the final boolean state of the Cancel property after all handlers were called. - -## Decision Outcome - -TBD. diff --git a/docs/decisions/0005-kernel-hooks-phase1.md b/docs/decisions/0005-kernel-hooks-phase1.md new file mode 100644 index 0000000000..a1c66c13b9 --- /dev/null +++ b/docs/decisions/0005-kernel-hooks-phase1.md @@ -0,0 +1,244 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: accepted +contact: rogerbarreto +date: 2023-05-29 +deciders: rogerbarreto, shawncal, stephentoub +consulted: +informed: +--- + +# Kernel/Function Handlers - Phase 1 + +## Context and Problem Statement + +A Kernel function caller needs to be able to handle/intercept any function execution in the Kernel before and after it was attempted. Allowing it to modify the prompt, abort the execution, or modify the output and many other scenarios as follows: + +- Pre-Execution / Function Invoking + + - Get: SKContext + - Set: Modify input parameters sending to the function + - Set: Abort/Cancel pipeline execution + - Set: Skip function execution + +- Post-Execution / Function Invoked + + - Get: LLM Model Result (Tokens Usage, Stop Sequence, ...) + - Get: SKContext + - Get: Output parameters + - Set: Modify output parameters content (before returning the output) + - Set: Cancel pipeline execution + - Set: Repeat function execution + +## Out of Scope (Will be in phase 2) + +- Pre-Execution / Function Invoking + + - Get: Rendered Prompt + - Get: Current settings used + - Set: Modify the Rendered Prompt + +- Post-Execution / Function Invoked + - Get: Rendered Prompt + - Get: Current settings used + +## Decision Drivers + +- Architecture changes and the associated decision making process should be transparent to the community. +- Decision records are stored in the repository and are easily discoverable for teams involved in the various language ports. +- Simple, Extensible and easy to understand. + +## Considered Options + +1. Callback Registration + Recursive +2. Single Callback +3. Event Based Registration +4. Middleware +5. ISKFunction Event Support Interfaces + +## Pros and Cons of the Options + +### 1. Callback Registration Recursive Delegate (Kernel, Plan, Function) + +- Specified on plan and function level as a configuration be able to specify what are the callback Handlers that will be triggered. + +Pros: + +- Common pattern for observing and also changing data exposed as parameter into the delegate signature for (Get/Set) scenarios +- Registering a callback gives back the registration object that can be used to cancel the execution of the function in the future. +- Recursive approach, allows to register multiple callbacks for the same event, and also allows to register callbacks on top of pre existing callbacks. + +Cons: + +- Registrations may use more memory and might not be garbage collected in the recursive approach, only when the function or the plan is disposed. + +### 2. Single Callback Delegate (Kernel, Plan, Function) + +- Specified on kernel level as a configuration be able to specify what are the callback Handlers that will be triggered. + - Specified on function creation: As part of the function constructor be able to specify what are the callback Handlers that will be triggered. + - Specified on function invocation: As part of the function invoke be able to specify what are the callback Handlers as a parameter that will be triggered. + +Pros: + +- Common pattern for observing and also changing data exposed as parameter into the delegate signature for (Get/Set) scenarios + +Cons: + +- Limited to only one method observing a specific event (Pre Post and InExecution). - Function When used as parameter, three new parameters would be needed as part of the function. (Specified on function invocation) - Extra Cons on + +### 3. Event Base Registration (Kernel only) + +Expose events on both IKernel and ISKFunction that the call can can be observing to interact. + +Pros: + +- Multiple Listeners can registered for the same event +- Listeners can be registered and unregistered at will +- Common pattern (EventArgs) for observing and also changing data exposed as parameter into the event signature for (Get/Set) scenarios + +Cons: + +- Event handlers are void, making the EventArgs by reference the only way to modify the data. +- Not clear how supportive is this approach for asynchronous pattern/multi threading +- Won't support `ISKFunction.InvokeAsync` + +### 4. Middleware (Kernel Only) + +Specified on Kernel level, and would only be used using IKernel.RunAsync operation, this pattern would be similar to asp.net core middlewares, running the pipelines with a context and a requestdelegate next for controlling (Pre/Post conditions) + +Pros: + +- Common pattern for handling Pre/Post Setting/Filtering data + +Cons: + +- Functions can run on their own instance, middlewares suggest more complexity and the existence of an external container/manager (Kernel) to intercept/observe function calls. + +### 5. ISKFunction Event Support Interfaces + + ```csharp + class Kernel : IKernel + { + RunAsync() { + var functionInvokingArgs = await this.TriggerEvent(this.FunctionInvoking, skFunction, context); + + var functionResult = await skFunction.InvokeAsync(context, cancellationToken: cancellationToken); + + var functionInvokedArgs = await this.TriggerEvent( + this.FunctionInvoked, + skFunction, + context); + } + + private TEventArgs? TriggerEvent(EventHandler? eventHandler, ISKFunction function, SKContext context) where TEventArgs : SKEventArgs + { + if (eventHandler is null) + { + return null; + } + + if (function is ISKFunctionEventSupport supportedFunction) + { + var eventArgs = await supportedFunction.PrepareEventArgsAsync(context); + eventHandler.Invoke(this, eventArgs); + return eventArgs; + } + + // Think about allowing to add data with the extra interface. + + // If a function don't support the specific event we can: + return null; // Ignore or Throw. + throw new NotSupportedException($"The provided function \"{function.Name}\" does not supports and implements ISKFunctionHandles<{typeof(TEventArgs).Name}>"); + } + } + + public interface ISKFunctionEventSupport where TEventArgs : SKEventArgs + { + Task PrepareEventArgsAsync(SKContext context, TEventArgs? eventArgs = null); + } + + class SemanticFunction : ISKFunction, + ISKFunctionEventSupport, + ISKFunctionEventSupport + { + + public FunctionInvokingEventArgs PrepareEventArgsAsync(SKContext context, FunctionInvokingEventArgs? eventArgs = null) + { + var renderedPrompt = await this.RenderPromptTemplateAsync(context); + context.Variables.Set(SemanticFunction.RenderedPromptKey, renderedPrompt); + + return new SemanticFunctionInvokingEventArgs(this.Describe(), context); + // OR Metadata Dictionary + return new FunctionInvokingEventArgs(this.Describe(), context, new Dictionary() { { RenderedPrompt, renderedPrompt } }); + } + + public FunctionInvokedEventArgs PrepareEventArgsAsync(SKContext context, FunctionInvokedEventArgs? eventArgs = null) + { + return Task.FromResult(new SemanticFunctionInvokedEventArgs(this.Describe(), context)); + } + } + + public sealed class SemanticFunctionInvokedEventArgs : FunctionInvokedEventArgs + { + public SemanticFunctionInvokedEventArgs(FunctionDescription functionDescription, SKContext context) + : base(functionDescription, context) + { + _context = context; + Metadata[RenderedPromptKey] = this._context.Variables[RenderedPromptKey]; + } + + public string? RenderedPrompt => this.Metadata[RenderedPromptKey]; + + } + + public sealed class SemanticFunctionInvokingEventArgs : FunctionInvokingEventArgs + { + public SemanticFunctionInvokingEventArgs(FunctionDescription functionDescription, SKContext context) + : base(functionDescription, context) + { + _context = context; + } + public string? RenderedPrompt => this._context.Variables[RenderedPromptKey]; + } + ``` + +### Pros and Cons + +Pros: + +- `Kernel` is not aware of `SemanticFunction` implementation details or any other `ISKFunction` implementation +- Extensible to show dedicated EventArgs per custom `ISKFunctions` implementation, including prompts for semantic functions +- Extensible to support future events on the Kernel thru the `ISKFunctionEventSupport` interface +- Functions can have their own EventArgs specialization. +- Interface is optional, so custom `ISKFunctions` can choose to implement it or not + +Cons: + +- Any custom functions now will have to responsibility implement the `ISKFunctionEventSupport` interface if they want to support events. +- `Kernel` will have to check if the function implements the interface or not, and if not, it will have to throw an exception or ignore the event. +- Functions implementations that once were limited to InvokeAsync now need to be scattered across multiple places and handle the state of the execution related to content that needs to be get at the beginning or at the end of the invocation. + +## Main Questions + +- Q: Post Execution Handlers should execute right after the LLM result or before the end of the function execution itself? + A: Currently post execution Handlers are executed after function execution. + +- Q: Should Pre/Post Handlers be many (pub/sub) allowing registration/deregistration? + A: By using the standard .NET event implementation, this already supports multiple registrations as well as deregistrations managed by the caller. + +- Q: Setting Handlers on top of pre existing Handlers should be allowed or throw an error? + A: By using the standard .NET event implementation, the standard behavior will not throw an error and will execute all the registered handlers. + +- Q: Setting Handlers on Plans should automatically cascade this Handlers for all the inner steps + overriding existing ones in the process? + A: Handlers will be triggered before and after each step is executed the same way the Kernel RunAsync pipeline works. + +- Q: When a pre function execution handler intents to cancel the execution, should further handlers in the chain be called or not? + A: Currently the standard .net behavior is to call all the registered handlers. This way function execution will solely depends on the final state of the Cancellation Request after all handlers were called. + +## Decision Outcome + +Chosen option: **3. Event Base Registration (Kernel only)** + +This approach is the simplest and take the benefits of the standard .NET event implementation. + +Further changes will be implemented to fully support all the scenarios in phase 2. diff --git a/docs/decisions/0012-kernel-service-registration.md b/docs/decisions/0012-kernel-service-registration.md index ea7a524e37..db2e8ac144 100644 --- a/docs/decisions/0012-kernel-service-registration.md +++ b/docs/decisions/0012-kernel-service-registration.md @@ -4,7 +4,7 @@ status: accepted contact: dmytrostruk date: 2023-10-03 deciders: dmytrostruk -consulted: semenshi, rbarreto, markwallace +consulted: semenshi, rbarreto, markwallace-microsoft informed: --- # Kernel Service Registration diff --git a/docs/decisions/0014-chat-completion-roles-in-prompt.md b/docs/decisions/0014-chat-completion-roles-in-prompt.md index 046db755ec..3f4b39b12f 100644 --- a/docs/decisions/0014-chat-completion-roles-in-prompt.md +++ b/docs/decisions/0014-chat-completion-roles-in-prompt.md @@ -3,7 +3,7 @@ status: accepted contact: semenshi date: 2023-10-23 -deciders: markwallace, mabolan +deciders: markwallace-microsoft, mabolan consulted: informed: --- From 3f36417642680399ccd75a6bccf5f417370f873e Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 1 Nov 2023 05:25:47 -0700 Subject: [PATCH 23/24] ADR 0014: Tiny typo fix (not -> no) (#3352) ### Motivation and Context ### Description Fixes a tiny typo. ### Contribution Checklist - [x] I didn't break anyone :smile: Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- docs/decisions/0014-custom-prompt-template-formats.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/0014-custom-prompt-template-formats.md b/docs/decisions/0014-custom-prompt-template-formats.md index 5a733ce181..62ee7b44da 100644 --- a/docs/decisions/0014-custom-prompt-template-formats.md +++ b/docs/decisions/0014-custom-prompt-template-formats.md @@ -43,7 +43,7 @@ Console.WriteLine(result.GetValue()); ``` We have an extension method `var kindOfDay = kernel.CreateSemanticFunction(promptTemplate);` to simplify the process to create and register a semantic function but the expanded format is shown above to highlight the dependency on `kernel.PromptTemplateEngine`. -Also the `BasicPromptTemplateEngine` is the default prompt template engine and will be loaded automatically if the package is available and not other prompt template engine is specified. +Also the `BasicPromptTemplateEngine` is the default prompt template engine and will be loaded automatically if the package is available and no other prompt template engine is specified. Some issues with this: From f3df736d7b888bf06c919d9b99651ee44f29b205 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:57:56 +0000 Subject: [PATCH 24/24] Update ADRs 0016 and 0017 (#3359) ### Motivation and Context 1. Fix ADR numbering 2. Fix some typos and improve explanation's 3. Update sample code to match the implementation ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- ...mantic-function-multiple-model-support.md} | 109 +++++++++--------- ...=> 0017-custom-prompt-template-formats.md} | 12 +- 2 files changed, 58 insertions(+), 63 deletions(-) rename docs/decisions/{0015-semantic-function-multiple-model-support.md => 0016-semantic-function-multiple-model-support.md} (53%) rename docs/decisions/{0014-custom-prompt-template-formats.md => 0017-custom-prompt-template-formats.md} (94%) diff --git a/docs/decisions/0015-semantic-function-multiple-model-support.md b/docs/decisions/0016-semantic-function-multiple-model-support.md similarity index 53% rename from docs/decisions/0015-semantic-function-multiple-model-support.md rename to docs/decisions/0016-semantic-function-multiple-model-support.md index 318285639f..bb2a3a72ae 100644 --- a/docs/decisions/0015-semantic-function-multiple-model-support.md +++ b/docs/decisions/0016-semantic-function-multiple-model-support.md @@ -11,27 +11,26 @@ informed: ## Context and Problem Statement -Developers need to be able to use multiple models e.g., using chat completion together with embeddings. +Developers need to be able to use multiple models simultaneously e.g., using GPT4 for certain prompts and GPT3.5 for others to reduce cost. ## Use Cases -In scope for Semantic Kernel V1.0 +In scope for Semantic Kernel V1.0 is the ability to select AI Service and Model Request Settings: -* Select Model Request Settings by Service Id. - * A Service Id uniquely identifies a registered AI Service and is typically defined in the scope of an application. -* Select AI Service and Model Request Settings By Developer Defined Strategy. - * A Developer Defined Strategy is a code first approach where a developer provides the logic. +1. By service id. + * A Service id uniquely identifies a registered AI Service and is typically defined in the scope of an application. +1. By developer defined strategy. + * A _developer defined strategy_ is a code first approach where a developer provides the logic. +1. By model id. + * A model id uniquely identifies a Large Language Model. Multiple AI service providers can support the same LLM. +1. By arbitrary AI service attributes + * E.g. an AI service can define a provider id which uniquely identifies an AI provider e.g. "Azure OpenAI", "OpenAI", "Hugging Face" -Out of scope for V1.0 - -* Select Model Request Settings by Model Id. - * A Model Id uniquely identifies a Large Language Model. Multiple AI service providers can support the same LLM. -* Select Model Request Settings by Provider Id and Model Id - * A Provider Id uniquely identifies an AI provider e.g. "Azure OpenAI", "OpenAI", "Hugging Face" +**This ADR focuses on items 1 & 2 in the above list. To implement 3 & 4 we need to provide the ability to store `AIService` metadata.** ## Decision Outcome -Support just use cases listed in this ADR. +Support use cases 1 & 2 listed in this ADR and create separate ADR to add support for AI service metadata. ## Descriptions of the Use Cases @@ -46,33 +45,31 @@ In the example below the semantic function is executed with "AzureText" using `m ```csharp // Configure a Kernel with multiple LLM's -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureTextCompletionService(deploymentName: aoai.DeploymentName, endpoint: aoai.Endpoint, serviceId: "AzureText", apiKey: aoai.ApiKey) .WithAzureChatCompletionService(deploymentName: aoai.ChatDeploymentName, endpoint: aoai.Endpoint, serviceId: "AzureChat", apiKey: aoai.ApiKey) .WithOpenAITextCompletionService(modelId: oai.ModelId, - serviceId: "OpenAIText", apiKey: oai.ApiKey) + serviceId: "OpenAIText", apiKey: oai.ApiKey, setAsDefault: true) .WithOpenAIChatCompletionService(modelId: oai.ChatModelId, serviceId: "OpenAIChat", apiKey: oai.ApiKey, setAsDefault: true) .Build(); // Configure semantic function with multiple LLM request settings -string configPayload = @"{ - ""schema"": 1, - ""description"": ""Hello AI, what can you do for me?"", - ""models"": [ - { ""service_id"": ""AzureText"", ""max_tokens"": 60 }, - { ""service_id"": ""AzureChat"", ""max_tokens"": 120 }, - { ""service_id"": ""OpenAIText"", ""max_tokens"": 180 }, - { ""service_id"": ""OpenAIChat"", ""max_tokens"": 240 } - ] -}"; -var templateConfig = JsonSerializer.Deserialize(configPayload); -var func = kernel.CreateSemanticFunction(prompt, config: templateConfig!, "HelloAI"); +var modelSettings = new List +{ + new OpenAIRequestSettings() { ServiceId = "AzureText", MaxTokens = 60 }, + new OpenAIRequestSettings() { ServiceId = "AzureChat", MaxTokens = 120 }, + new OpenAIRequestSettings() { ServiceId = "OpenAIText", MaxTokens = 180 }, + new OpenAIRequestSettings() { ServiceId = "OpenAIChat", MaxTokens = 240 } +}; +var prompt = "Hello AI, what can you do for me?"; +var promptTemplateConfig = new PromptTemplateConfig() { ModelSettings = modelSettings }; +var func = kernel.CreateSemanticFunction(prompt, config: promptTemplateConfig, "HelloAI"); -// Semantic function is executed with OpenAIChat using max_tokens=240 +// Semantic function is executed with AzureText using max_tokens=60 result = await kernel.RunAsync(func); ``` @@ -82,49 +79,53 @@ The interface is defined as follows: ```csharp public interface IAIServiceSelector { - (T?, AIRequestSettings?) SelectAIService(IAIServiceProvider serviceProvider, IReadOnlyList? modelSettings) where T : IAIService; + (T?, AIRequestSettings?) SelectAIService( + string renderedPrompt, + IAIServiceProvider serviceProvider, + IReadOnlyList? modelSettings) where T : IAIService; } ``` -A default `OrderedIAIServiceSelector` implementation is used which selects the AI service based on the order of the model request settings defined for the semantic function. -The implementation checks if a service exists which the corresponding service id and if it does it and the associated model request settings will be used. -In no model request settings are defined then the default text completion service is used. -A default set of request settings can be specified by leaving the service id undefined or empty, the first such default will be used. -If no default if specified and none of the specified services are available the operation will fail. +A default `OrderedIAIServiceSelector` implementation is provided which selects the AI service based on the order of the model request settings defined for the semantic function. + +* The implementation checks if a service exists which the corresponding service id and if it does it and the associated model request settings will be used. +* In no model request settings are defined then the default text completion service is used. +* A default set of request settings can be specified by leaving the service id undefined or empty, the first such default will be used. +* If no default if specified and none of the specified services are available the operation will fail. ### Select AI Service and Model Request Settings By Developer Defined Strategy -_As a developer using the Semantic Kernel I can provide factories which select the AI service and request settings used to execute my function so that I can dynamically control which AI service is used to execute my semantic function._ +_As a developer using the Semantic Kernel I can provide an implementation which selects the AI service and request settings used to execute my function so that I can dynamically control which AI service and settings are used to execute my semantic function._ -In this case the developer configures different settings based on the model id and provides an AI Service factory which determines which AI Service will be used when the semantic function is executed. -In the example below the semantic function is executed with whatever AI Service `myServiceFactory` returns. +In this case the developer configures different settings based on the service id and provides an AI Service Selector which determines which AI Service will be used when the semantic function is executed. +In the example below the semantic function is executed with whatever AI Service and AI Request Settings `MyAIServiceSelector` returns e.g. it will be possible to create an AI Service Selector that computes the token count of the rendered prompt and uses that to determine which service to use. ```csharp // Configure a Kernel with multiple LLM's -IKernel kernel = Kernel.Builder +IKernel kernel = new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) - .WithAzureTextCompletionService(deploymentName: aoai.DeploymentName, // text-davinci-003 + .WithAzureTextCompletionService(deploymentName: aoai.DeploymentName, endpoint: aoai.Endpoint, serviceId: "AzureText", apiKey: aoai.ApiKey) - .WithAzureChatCompletionService(deploymentName: aoai.ChatDeploymentName, // gpt-35-turbo + .WithAzureChatCompletionService(deploymentName: aoai.ChatDeploymentName, endpoint: aoai.Endpoint, serviceId: "AzureChat", apiKey: aoai.ApiKey) - .WithOpenAITextCompletionService(modelId: oai.ModelId, // text-davinci-003 + .WithOpenAITextCompletionService(modelId: oai.ModelId, serviceId: "OpenAIText", apiKey: oai.ApiKey, setAsDefault: true) - .WithOpenAIChatCompletionService(modelId: oai.ChatModelId, // gpt-3.5-turbo - serviceId: "OpenAIChat", apiKey: oai.ApiKey) + .WithOpenAIChatCompletionService(modelId: oai.ChatModelId, + serviceId: "OpenAIChat", apiKey: oai.ApiKey, setAsDefault: true) + .WithAIServiceSelector(new MyAIServiceSelector()) .Build(); // Configure semantic function with multiple LLM request settings -string configPayload = @"{ - ""schema"": 1, - ""description"": ""Hello AI, what can you do for me?"", - ""models"": [ - { ""max_tokens"": 60 }, - { ""max_tokens"": 120 }, - { ""max_tokens"": 180 } - ] -}"; -var templateConfig = JsonSerializer.Deserialize(configPayload); -var func = kernel.CreateSemanticFunction(prompt, config: templateConfig!, "HelloAI"); +var modelSettings = new List +{ + new OpenAIRequestSettings() { ServiceId = "AzureText", MaxTokens = 60 }, + new OpenAIRequestSettings() { ServiceId = "AzureChat", MaxTokens = 120 }, + new OpenAIRequestSettings() { ServiceId = "OpenAIText", MaxTokens = 180 }, + new OpenAIRequestSettings() { ServiceId = "OpenAIChat", MaxTokens = 240 } +}; +var prompt = "Hello AI, what can you do for me?"; +var promptTemplateConfig = new PromptTemplateConfig() { ModelSettings = modelSettings }; +var func = kernel.CreateSemanticFunction(prompt, config: promptTemplateConfig, "HelloAI"); // Semantic function is executed with AI Service and AI request Settings dynamically determined result = await kernel.RunAsync(func, funcVariables); diff --git a/docs/decisions/0014-custom-prompt-template-formats.md b/docs/decisions/0017-custom-prompt-template-formats.md similarity index 94% rename from docs/decisions/0014-custom-prompt-template-formats.md rename to docs/decisions/0017-custom-prompt-template-formats.md index 62ee7b44da..755d19a754 100644 --- a/docs/decisions/0014-custom-prompt-template-formats.md +++ b/docs/decisions/0017-custom-prompt-template-formats.md @@ -50,7 +50,7 @@ Some issues with this: 1. You need to have a `Kernel` instance to create a semantic function, which is contrary to one of the goals of allow semantic functions to be created once and reused across multiple `Kernel` instances. 1. `Kernel` only supports a single `IPromptTemplateEngine` so we cannot support using multiple prompt templates at the same time. 1. `IPromptTemplateEngine` is stateless and must perform a parse of the template for each render -1. Our semantic function extension methods relay on our implementation of `IPromptTemplate` (i.e., `PromptTemplate`) which stores the template string and uses the `IPromptTemplateEngine` to render it every time. Note implementations of `IPromptTemplate` are currently stateful as they also store the parameters. +1. Our semantic function extension methods rely on our implementation of `IPromptTemplate` (i.e., `PromptTemplate`) which stores the template string and uses the `IPromptTemplateEngine` to render it every time. Note implementations of `IPromptTemplate` are currently stateful as they also store the parameters. #### Performance @@ -136,7 +136,6 @@ Some issues: 1. The `IPromptTemplate` interface is not used and causes confusion. 1. There is no way to allow developers to support multiple prompt template formats at the same time. - There is one implementation of `IPromptTemplate` provided in the Semantic Kernel core package. The `RenderAsync` implementation just delegates to the `IPromptTemplateEngine`. The `Parameters` list get's populated with the parameters defined in the `PromptTemplateConfig` and any missing variables defined in the template. @@ -185,7 +184,6 @@ In no particular order: ## Considered Options * Obsolete `IPromptTemplateEngine` and replace with `IPromptTemplateFactory`. -* ### Obsolete `IPromptTemplateEngine` and replace with `IPromptTemplateFactory` @@ -197,12 +195,8 @@ Below is an expanded example of how to create a semantic function from a prompt // Semantic function can be created once var promptTemplateFactory = new BasicPromptTemplateFactory(); string templateString = "Today is: {{time.Date}} Is it weekend time (weekend/not weekend)?"; -var promptTemplateConfig = new PromptTemplateConfig(); -// Line below will replace the commented out code -var promptTemplate = promptTemplateFactory.CreatePromptTemplate(templateString, promptTemplateConfig); +var promptTemplate = promptTemplateFactory.CreatePromptTemplate(templateString, new PromptTemplateConfig()); var kindOfDay = ISKFunction.CreateSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate) -// var promptTemplate = new PromptTemplate(promptTemplate, promptTemplateConfig, kernel.PromptTemplateEngine); -// var kindOfDay = kernel.RegisterSemanticFunction("KindOfDay", promptTemplateConfig, promptTemplate); // Create Kernel after creating the semantic function // Later we will support passing a function collection to the KernelBuilder @@ -214,7 +208,7 @@ IKernel kernel = Kernel.Builder kernel.ImportFunctions(new TimePlugin(), "time"); // Optionally register the semantic function with the Kernel -kernel.RegisterCustomFunction(kindOfDay); +// kernel.RegisterCustomFunction(kindOfDay); var result = await kernel.RunAsync(kindOfDay); Console.WriteLine(result.GetValue());