diff --git a/.github/workflows/check-coverage.ps1 b/.github/workflows/check-coverage.ps1 new file mode 100644 index 0000000000..39fec480cb --- /dev/null +++ b/.github/workflows/check-coverage.ps1 @@ -0,0 +1,54 @@ +param ( + [string]$JsonReportPath, + [double]$CoverageThreshold +) + +$jsonContent = Get-Content $JsonReportPath -Raw | ConvertFrom-Json +$coverageBelowThreshold = $false + +function Get-FormattedValue($number) { + $formattedNumber = "{0:N1}" -f $number + $icon = if ($number -ge $CoverageThreshold) { '✅' } else { '❌' } + + return "$formattedNumber% $icon" +} + +$lineCoverage = $jsonContent.summary.linecoverage +$branchCoverage = $jsonContent.summary.branchcoverage + +if ($lineCoverage -lt $CoverageThreshold -or $branchCoverage -lt $CoverageThreshold) { + $coverageBelowThreshold = $true +} + +$totalTableData = [PSCustomObject]@{ + 'Metric' = 'Total Coverage' + 'Line Coverage' = Get-FormattedValue $lineCoverage + 'Branch Coverage' = Get-FormattedValue $branchCoverage +} + +$totalTableData | Format-Table -AutoSize + +$assemblyTableData = @() + +foreach ($assembly in $jsonContent.coverage.assemblies) { + $assemblyName = $assembly.name + $assemblyLineCoverage = $assembly.coverage + $assemblyBranchCoverage = $assembly.branchcoverage + + if ($assemblyLineCoverage -lt $CoverageThreshold -or $assemblyBranchCoverage -lt $CoverageThreshold) { + $coverageBelowThreshold = $true + } + + $assemblyTableData += [PSCustomObject]@{ + 'Assembly Name' = $assemblyName + 'Line' = Get-FormattedValue $assemblyLineCoverage + 'Branch' = Get-FormattedValue $assemblyBranchCoverage + } +} + +$assemblyTableData | Format-Table -AutoSize + +if ($coverageBelowThreshold) { + Write-Host "Code coverage is lower than defined threshold: $CoverageThreshold. Stopping the task." + exit 1 +} diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index fc9f43ff86..755aef1f7d 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -12,6 +12,9 @@ on: merge_group: branches: ["main"] +env: + COVERAGE_THRESHOLD: 80 + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -82,7 +85,7 @@ jobs: run: | export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | grep -v -E "(Planners.Core.UnitTests.csproj|Experimental.Orchestration.Flow.UnitTests.csproj|Experimental.Assistants.UnitTests.csproj)" | tr '\n' ' ') for project in $UT_PROJECTS; do - dotnet test -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx + dotnet test -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" done - name: Run Integration Tests @@ -109,6 +112,20 @@ jobs: Bing__ApiKey: ${{ secrets.BING__APIKEY }} OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} + # Generate test reports and check coverage + - name: Generate test reports + uses: danielpalme/ReportGenerator-GitHub-Action@5.2.0 + with: + reports: "./TestResults/Coverage/**/coverage.cobertura.xml" + targetdir: "./TestResults/Reports" + reporttypes: "JsonSummary" + # Report for production packages only + assemblyfilters: "+Microsoft.SemanticKernel.Abstractions;+Microsoft.SemanticKernel.Core;+Microsoft.SemanticKernel.PromptTemplates.Handlebars;+Microsoft.SemanticKernel.Connectors.OpenAI;+Microsoft.SemanticKernel.Yaml;" + + - name: Check coverage + shell: pwsh + run: .github/workflows/check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD + # This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed dotnet-build-and-test-check: if: always() diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index a95145ec25..0807f280cc 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -50,7 +50,6 @@ - @@ -66,7 +65,6 @@ - diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 5cf185efc5..3dd0ad6a37 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -118,9 +118,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10 src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Core", "src\Plugins\Plugins.Core\Plugins.Core.csproj", "{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Type", "Type", "{E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\src\Type\TypeExtensions.cs = src\InternalUtilities\src\Type\TypeExtensions.cs + EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCalcPlugins", "samples\NCalcPlugins\NCalcPlugins.csproj", "{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Core", "src\Plugins\Plugins.Core\Plugins.Core.csproj", "{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelemetryExample", "samples\TelemetryExample\TelemetryExample.csproj", "{C754950A-E16C-4F96-9CC7-9328E361B5AF}" EndProject @@ -353,11 +356,6 @@ Global {0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}.Publish|Any CPU.Build.0 = Publish|Any CPU {0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}.Release|Any CPU.Build.0 = Release|Any CPU - {E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {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 {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 @@ -500,8 +498,8 @@ Global {B00AD427-0047-4850-BEF9-BA8237EA9D8B} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {1C19D805-3573-4477-BF07-40180FCDE1BD} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} + {E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF} = {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} {C754950A-E16C-4F96-9CC7-9328E361B5AF} = {FA3720F1-C99A-49B2-9577-A940257098BF} {E07608CC-D710-4655-BB9E-D22CF3CDD193} = {24503383-A8C4-4255-9998-28D70FE8E99A} {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} diff --git a/dotnet/code-coverage.ps1 b/dotnet/code-coverage.ps1 new file mode 100644 index 0000000000..108dbdffa7 --- /dev/null +++ b/dotnet/code-coverage.ps1 @@ -0,0 +1,60 @@ +# This script is for local use to analyze code coverage in more detail using HTML report. + +Param( + [switch]$ProdPackagesOnly = $false +) + +# Generate a timestamp for the current date and time +$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + +# Define paths +$scriptPath = Get-Item -Path $PSScriptRoot +$coverageOutputPath = Join-Path $scriptPath "TestResults\Coverage\$timestamp" +$reportOutputPath = Join-Path $scriptPath "TestResults\Reports\$timestamp" + +# Create output directories +New-Item -ItemType Directory -Force -Path $coverageOutputPath +New-Item -ItemType Directory -Force -Path $reportOutputPath + +# Find tests for projects ending with 'UnitTests.csproj' +$testProjects = Get-ChildItem $scriptPath -Filter "*UnitTests.csproj" -Recurse + +foreach ($project in $testProjects) { + $testProjectPath = $project.FullName + Write-Host "Running tests for project: $($testProjectPath)" + + # Run tests + dotnet test $testProjectPath ` + --collect:"XPlat Code Coverage" ` + --results-directory:$coverageOutputPath ` + +} + +# Install required tools +& dotnet tool install -g coverlet.console +& dotnet tool install -g dotnet-reportgenerator-globaltool + +# Generate HTML report +if ($ProdPackagesOnly) { + $assemblies = @( + "+Microsoft.SemanticKernel.Abstractions", + "+Microsoft.SemanticKernel.Core", + "+Microsoft.SemanticKernel.PromptTemplates.Handlebars", + "+Microsoft.SemanticKernel.Connectors.OpenAI", + "+Microsoft.SemanticKernel.Yaml" + ) + + $assemblyFilters = $assemblies -join ";" + + # Generate report for production assemblies only + & reportgenerator -reports:"$coverageOutputPath/**/coverage.cobertura.xml" -targetdir:$reportOutputPath -reporttypes:Html -assemblyfilters:$assemblyFilters +} +else { + & reportgenerator -reports:"$coverageOutputPath/**/coverage.cobertura.xml" -targetdir:$reportOutputPath -reporttypes:Html +} + +Write-Host "Code coverage report generated at: $reportOutputPath" + +# Open report +$reportIndexHtml = Join-Path $reportOutputPath "index.html" +Invoke-Item -Path $reportIndexHtml diff --git a/dotnet/samples/.editorconfig b/dotnet/samples/.editorconfig index 85856ed89b..7fb69b7489 100644 --- a/dotnet/samples/.editorconfig +++ b/dotnet/samples/.editorconfig @@ -21,4 +21,5 @@ resharper_condition_is_always_true_or_false_according_to_nullable_api_contract_h resharper_inconsistent_naming_highlighting = none # InconsistentNaming resharper_equal_expression_comparison_highlighting = none # EqualExpressionComparison resharper_check_namespace_highlighting = none # CheckNamespace -resharper_arrange_object_creation_when_type_not_evident_highlighting = none # Disable "Arrange object creation when type is not evident" highlighting \ No newline at end of file +resharper_arrange_object_creation_when_type_not_evident_highlighting = none # Disable "Arrange object creation when type is not evident" highlighting +resharper_arrange_this_qualifier_highlighting = none # Disable "Arrange 'this.' qualifier" highlighting \ No newline at end of file diff --git a/dotnet/samples/KernelSyntaxExamples/BaseTest.cs b/dotnet/samples/KernelSyntaxExamples/BaseTest.cs new file mode 100644 index 0000000000..8e03d5ab91 --- /dev/null +++ b/dotnet/samples/KernelSyntaxExamples/BaseTest.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Configuration; +using RepoUtils; +using Xunit.Abstractions; + +namespace Examples; + +public abstract class BaseTest +{ + protected ITestOutputHelper Output { get; } + + protected BaseTest(ITestOutputHelper output) + { + this.Output = output; + LoadUserSecrets(); + } + + private static void LoadUserSecrets() + { + IConfigurationRoot configRoot = new ConfigurationBuilder() + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + TestConfiguration.Initialize(configRoot); + } + + /// + /// This method can be substituted by Console.WriteLine when used in Console apps. + /// + /// Target object to write + protected void WriteLine(object? target = null) + { + this.Output.WriteLine(target ?? string.Empty); + } + + /// + /// Current interface ITestOutputHelper does not have a Write method. This extension method adds it to make it analogous to Console.Write when used in Console apps. + /// + /// Target object to write + protected void Write(object? target = null) + { + this.Output.WriteLine(target ?? string.Empty); + } +} diff --git a/dotnet/samples/KernelSyntaxExamples/Example01_MethodFunctions.cs b/dotnet/samples/KernelSyntaxExamples/Example01_MethodFunctions.cs index ddf0bd3455..d3f113b5f8 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example01_MethodFunctions.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example01_MethodFunctions.cs @@ -1,14 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel.Plugins.Core; +using Xunit; +using Xunit.Abstractions; -public static class Example01_MethodFunctions +namespace Examples; + +public class Example01_MethodFunctions : BaseTest { - public static Task RunAsync() + [Fact] + public Task RunAsync() { - Console.WriteLine("======== Functions ========"); + this.WriteLine("======== Functions ========"); // Load native plugin var text = new TextPlugin(); @@ -16,8 +20,12 @@ public static class Example01_MethodFunctions // Use function without kernel var result = text.Uppercase("ciao!"); - Console.WriteLine(result); + this.WriteLine(result); return Task.CompletedTask; } + + public Example01_MethodFunctions(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example03_Arguments.cs b/dotnet/samples/KernelSyntaxExamples/Example03_Arguments.cs index 039488ad20..d157946bca 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example03_Arguments.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example03_Arguments.cs @@ -5,13 +5,17 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Plugins; +using Xunit; +using Xunit.Abstractions; +namespace Examples; // This example shows how to use kernel arguments when invoking functions. -public static class Example03_Arguments +public class Example03_Arguments : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Arguments ========"); + this.WriteLine("======== Arguments ========"); Kernel kernel = new(); var textPlugin = kernel.ImportPluginFromType(); @@ -26,16 +30,20 @@ public static class Example03_Arguments // Specify and get the value type as generic parameter string? resultValue = await kernel.InvokeAsync(textPlugin["AppendDay"], arguments); - Console.WriteLine($"string -> {resultValue}"); + this.WriteLine($"string -> {resultValue}"); // If you need to access the result metadata, you can use the non-generic version to get the FunctionResult FunctionResult functionResult = await kernel.InvokeAsync(textPlugin["AppendDay"], arguments); var metadata = functionResult.Metadata; // Specify the type from the FunctionResult - Console.WriteLine($"FunctionResult.GetValue() -> {functionResult.GetValue()}"); + this.WriteLine($"FunctionResult.GetValue() -> {functionResult.GetValue()}"); // FunctionResult.ToString() automatically converts the result to string - Console.WriteLine($"FunctionResult.ToString() -> {functionResult}"); + this.WriteLine($"FunctionResult.ToString() -> {functionResult}"); + } + + public Example03_Arguments(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example05_InlineFunctionDefinition.cs b/dotnet/samples/KernelSyntaxExamples/Example05_InlineFunctionDefinition.cs index e20a76430c..92ad2f7e89 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example05_InlineFunctionDefinition.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example05_InlineFunctionDefinition.cs @@ -4,19 +4,24 @@ using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; -public static class Example05_InlineFunctionDefinition +namespace Examples; + +public class Example05_InlineFunctionDefinition : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Inline Function Definition ========"); + this.WriteLine("======== Inline Function Definition ========"); string openAIModelId = TestConfiguration.OpenAI.ChatModelId; string openAIApiKey = TestConfiguration.OpenAI.ApiKey; if (openAIModelId is null || openAIApiKey is null) { - Console.WriteLine("OpenAI credentials not found. Skipping example."); + this.WriteLine("OpenAI credentials not found. Skipping example."); return; } @@ -49,14 +54,18 @@ Event: {{$input}} var excuseFunction = kernel.CreateFunctionFromPrompt(promptTemplate, new OpenAIPromptExecutionSettings() { MaxTokens = 100, Temperature = 0.4, TopP = 1 }); var result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = "I missed the F1 final race" }); - Console.WriteLine(result.GetValue()); + this.WriteLine(result.GetValue()); result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = "sorry I forgot your birthday" }); - Console.WriteLine(result.GetValue()); + this.WriteLine(result.GetValue()); var fixedFunction = kernel.CreateFunctionFromPrompt($"Translate this date {DateTimeOffset.Now:f} to French format", new OpenAIPromptExecutionSettings() { MaxTokens = 100 }); result = await kernel.InvokeAsync(fixedFunction); - Console.WriteLine(result.GetValue()); + this.WriteLine(result.GetValue()); + } + + public Example05_InlineFunctionDefinition(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs b/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs index 0f8f81ea66..72b5a8f5bb 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example06_TemplateLanguage.cs @@ -1,27 +1,31 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Plugins.Core; +using Xunit; +using Xunit.Abstractions; -public static class Example06_TemplateLanguage +namespace Examples; + +public class Example06_TemplateLanguage : BaseTest { /// /// Show how to invoke a Method Function written in C# /// from a Prompt Function written in natural language /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== TemplateLanguage ========"); + this.WriteLine("======== TemplateLanguage ========"); string openAIModelId = TestConfiguration.OpenAI.ChatModelId; string openAIApiKey = TestConfiguration.OpenAI.ApiKey; if (openAIModelId == null || openAIApiKey == null) { - Console.WriteLine("OpenAI credentials not found. Skipping example."); + this.WriteLine("OpenAI credentials not found. Skipping example."); return; } @@ -46,19 +50,19 @@ Is it weekend time (weekend/not weekend)? "; // This allows to see the prompt before it's sent to OpenAI - Console.WriteLine("--- Rendered Prompt"); + this.WriteLine("--- Rendered Prompt"); var promptTemplateFactory = new KernelPromptTemplateFactory(); var promptTemplate = promptTemplateFactory.Create(new PromptTemplateConfig(FunctionDefinition)); var renderedPrompt = await promptTemplate.RenderAsync(kernel); - Console.WriteLine(renderedPrompt); + this.WriteLine(renderedPrompt); // Run the prompt / prompt function var kindOfDay = kernel.CreateFunctionFromPrompt(FunctionDefinition, new OpenAIPromptExecutionSettings() { MaxTokens = 100 }); // Show the result - Console.WriteLine("--- Prompt Function result"); + this.WriteLine("--- Prompt Function result"); var result = await kernel.InvokeAsync(kindOfDay); - Console.WriteLine(result.GetValue()); + this.WriteLine(result.GetValue()); /* OUTPUT: @@ -81,4 +85,8 @@ Is it weekend time (weekend/not weekend)? } */ } + + public Example06_TemplateLanguage(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example07_BingAndGooglePlugins.cs b/dotnet/samples/KernelSyntaxExamples/Example07_BingAndGooglePlugins.cs index 8f8de207f8..d2745f898b 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example07_BingAndGooglePlugins.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example07_BingAndGooglePlugins.cs @@ -7,22 +7,27 @@ using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Plugins.Web; using Microsoft.SemanticKernel.Plugins.Web.Bing; using Microsoft.SemanticKernel.Plugins.Web.Google; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// The example shows how to use Bing and Google to search for current data /// you might want to import into your system, e.g. providing AI prompts with /// recent information, or for AI to generate recent information to display to users. /// -public static class Example07_BingAndGooglePlugins +public class Example07_BingAndGooglePlugins : BaseTest { - public static async Task RunAsync() + [Fact(Skip = "Setup Credentials")] + public async Task RunAsync() { string openAIModelId = TestConfiguration.OpenAI.ChatModelId; string openAIApiKey = TestConfiguration.OpenAI.ApiKey; if (openAIModelId == null || openAIApiKey == null) { - Console.WriteLine("OpenAI credentials not found. Skipping example."); + this.WriteLine("OpenAI credentials not found. Skipping example."); return; } @@ -36,7 +41,7 @@ public static class Example07_BingAndGooglePlugins string bingApiKey = TestConfiguration.Bing.ApiKey; if (bingApiKey == null) { - Console.WriteLine("Bing credentials not found. Skipping example."); + this.WriteLine("Bing credentials not found. Skipping example."); } else { @@ -53,7 +58,7 @@ public static class Example07_BingAndGooglePlugins if (googleApiKey == null || googleSearchEngineId == null) { - Console.WriteLine("Google credentials not found. Skipping example."); + this.WriteLine("Google credentials not found. Skipping example."); } else { @@ -62,22 +67,23 @@ public static class Example07_BingAndGooglePlugins searchEngineId: googleSearchEngineId); var google = new WebSearchEnginePlugin(googleConnector); kernel.ImportPluginFromObject(new WebSearchEnginePlugin(googleConnector), "google"); + // ReSharper disable once ArrangeThisQualifier await Example1Async(kernel, "google"); } } - private static async Task Example1Async(Kernel kernel, string searchPluginName) + private async Task Example1Async(Kernel kernel, string searchPluginName) { - Console.WriteLine("======== Bing and Google Search Plugins ========"); + this.WriteLine("======== Bing and Google Search Plugins ========"); // Run var question = "What's the largest building in the world?"; var function = kernel.Plugins[searchPluginName]["search"]; var result = await kernel.InvokeAsync(function, new() { ["query"] = question }); - Console.WriteLine(question); - Console.WriteLine($"----{searchPluginName}----"); - Console.WriteLine(result.GetValue()); + this.WriteLine(question); + this.WriteLine($"----{searchPluginName}----"); + this.WriteLine(result.GetValue()); /* OUTPUT: @@ -92,9 +98,9 @@ public static class Example07_BingAndGooglePlugins */ } - private static async Task Example2Async(Kernel kernel) + private async Task Example2Async(Kernel kernel) { - Console.WriteLine("======== Use Search Plugin to answer user questions ========"); + this.WriteLine("======== Use Search Plugin to answer user questions ========"); const string SemanticFunction = @"Answer questions only when you know the facts or the information is provided. When you don't have sufficient information you reply with a list of commands to find the information needed. @@ -130,7 +136,7 @@ Question: {{ $question }}. Answer: "; var question = "Who is the most followed person on TikTok right now? What's the exchange rate EUR:USD?"; - Console.WriteLine(question); + this.WriteLine(question); var oracle = kernel.CreateFunctionFromPrompt(SemanticFunction, new OpenAIPromptExecutionSettings() { MaxTokens = 150, Temperature = 0, TopP = 1 }); @@ -148,11 +154,11 @@ Answer: "; var promptTemplateFactory = new KernelPromptTemplateFactory(); var promptTemplate = promptTemplateFactory.Create(new PromptTemplateConfig(result)); - Console.WriteLine("---- Fetching information from Bing..."); + this.WriteLine("---- Fetching information from Bing..."); var information = await promptTemplate.RenderAsync(kernel); - Console.WriteLine("Information found:"); - Console.WriteLine(information); + this.WriteLine("Information found:"); + this.WriteLine(information); // Run the prompt function again, now including information from Bing answer = await kernel.InvokeAsync(oracle, new KernelArguments() @@ -164,11 +170,11 @@ Answer: "; } else { - Console.WriteLine("AI had all the information, no need to query Bing."); + this.WriteLine("AI had all the information, no need to query Bing."); } - Console.WriteLine("---- ANSWER:"); - Console.WriteLine(answer.GetValue()); + this.WriteLine("---- ANSWER:"); + this.WriteLine(answer.GetValue()); /* OUTPUT: @@ -188,4 +194,8 @@ Answer: "; * The exchange rate for EUR to USD is 1.1037097 US Dollars for 1 Euro. */ } + + public Example07_BingAndGooglePlugins(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs b/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs index 15d648f1ea..df66d963fa 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example08_RetryHandler.cs @@ -7,11 +7,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use a retry handler within a Semantic Kernel -public static class Example08_RetryHandler +public class Example08_RetryHandler : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a Kernel with the HttpClient IKernelBuilder builder = Kernel.CreateBuilder(); @@ -29,7 +34,7 @@ public static class Example08_RetryHandler var logger = kernel.LoggerFactory.CreateLogger(typeof(Example08_RetryHandler)); - const string Question = "How popular is the Polly library?"; + const string Question = "How do I add a standard resilience handler in IHttpClientBuilder??"; logger.LogInformation("Question: {Question}", Question); // The call to OpenAI will fail and be retried a few times before eventually failing. @@ -49,4 +54,8 @@ public static class Example08_RetryHandler logger.LogInformation("Error: {Message}", ex.Message); } } + + public Example08_RetryHandler(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs b/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs index ec872c21da..6574479ca4 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example09_FunctionTypes.cs @@ -11,16 +11,22 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using RepoUtils; +using Xunit; +using Xunit.Abstractions; -public static class Example09_FunctionTypes +namespace Examples; + +public class Example09_FunctionTypes : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Method Function types ========"); + this.WriteLine("======== Method Function types ========"); var builder = Kernel.CreateBuilder() .AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Warning)); + builder.Services.AddSingleton(this.Output); var kernel = builder.Build(); kernel.Culture = new CultureInfo("pt-BR"); @@ -36,7 +42,7 @@ public static class Example09_FunctionTypes await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.InputDateTimeWithStringResult)], new() { ["currentDate"] = DateTime.Now }); await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.NoInputTaskWithStringResult)]); await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.MultipleInputsWithVoidResult)], new() { ["x"] = "x string", ["y"] = 100, ["z"] = 1.5 }); - await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.ComplexInputWithStringResult)], new() { ["complexObject"] = new LocalExamplePlugin() }); + await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.ComplexInputWithStringResult)], new() { ["complexObject"] = new LocalExamplePlugin(this.Output) }); await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.InputStringTaskWithStringResult)], new() { ["echoInput"] = "return this" }); await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.InputStringTaskWithVoidResult)], new() { ["x"] = "x input" }); await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.NoInputWithFunctionResult)]); @@ -60,19 +66,30 @@ public static class Example09_FunctionTypes // You can also use the kernel.Plugins collection to invoke a function await kernel.InvokeAsync(kernel.Plugins["Examples"][nameof(LocalExamplePlugin.NoInputWithVoidResult)]); } + + public Example09_FunctionTypes(ITestOutputHelper output) : base(output) + { + } } // Task functions when are imported as plugins loose the "Async" suffix if present. #pragma warning disable IDE1006 // Naming Styles public class LocalExamplePlugin { + private readonly ITestOutputHelper _output; + + public LocalExamplePlugin(ITestOutputHelper output) + { + this._output = output; + } + /// /// Example of using a void function with no input /// [KernelFunction] public void NoInputWithVoidResult() { - Console.WriteLine($"Running {nameof(this.NoInputWithVoidResult)} -> No input"); + this._output.WriteLine($"Running {nameof(this.NoInputWithVoidResult)} -> No input"); } /// @@ -81,7 +98,7 @@ public class LocalExamplePlugin [KernelFunction] public Task NoInputTaskWithVoidResult() { - Console.WriteLine($"Running {nameof(this.NoInputTaskWithVoidResult)} -> No input"); + this._output.WriteLine($"Running {nameof(this.NoInputTaskWithVoidResult)} -> No input"); return Task.CompletedTask; } @@ -92,7 +109,7 @@ public class LocalExamplePlugin public string InputDateTimeWithStringResult(DateTime currentDate) { var result = currentDate.ToString(CultureInfo.InvariantCulture); - Console.WriteLine($"Running {nameof(this.InputDateTimeWithStringResult)} -> [currentDate = {currentDate}] -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.InputDateTimeWithStringResult)} -> [currentDate = {currentDate}] -> result: {result}"); return result; } @@ -103,7 +120,7 @@ public class LocalExamplePlugin public Task NoInputTaskWithStringResult() { var result = "string result"; - Console.WriteLine($"Running {nameof(this.NoInputTaskWithStringResult)} -> No input -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.NoInputTaskWithStringResult)} -> No input -> result: {result}"); return Task.FromResult(result); } @@ -113,7 +130,7 @@ public class LocalExamplePlugin [KernelFunction] public void MultipleInputsWithVoidResult(string x, int y, double z) { - Console.WriteLine($"Running {nameof(this.MultipleInputsWithVoidResult)} -> input: [x = {x}, y = {y}, z = {z}]"); + this._output.WriteLine($"Running {nameof(this.MultipleInputsWithVoidResult)} -> input: [x = {x}, y = {y}, z = {z}]"); } /// @@ -123,7 +140,7 @@ public class LocalExamplePlugin public string ComplexInputWithStringResult(object complexObject) { var result = complexObject.GetType().Name; - Console.WriteLine($"Running {nameof(this.ComplexInputWithStringResult)} -> input: [complexObject = {complexObject}] -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.ComplexInputWithStringResult)} -> input: [complexObject = {complexObject}] -> result: {result}"); return result; } @@ -133,7 +150,7 @@ public class LocalExamplePlugin [KernelFunction] public Task InputStringTaskWithStringResult(string echoInput) { - Console.WriteLine($"Running {nameof(this.InputStringTaskWithStringResult)} -> input: [echoInput = {echoInput}] -> result: {echoInput}"); + this._output.WriteLine($"Running {nameof(this.InputStringTaskWithStringResult)} -> input: [echoInput = {echoInput}] -> result: {echoInput}"); return Task.FromResult(echoInput); } @@ -143,7 +160,7 @@ public class LocalExamplePlugin [KernelFunction] public Task InputStringTaskWithVoidResult(string x) { - Console.WriteLine($"Running {nameof(this.InputStringTaskWithVoidResult)} -> input: [x = {x}]"); + this._output.WriteLine($"Running {nameof(this.InputStringTaskWithVoidResult)} -> input: [x = {x}]"); return Task.CompletedTask; } @@ -155,7 +172,7 @@ public class LocalExamplePlugin { var myInternalFunction = KernelFunctionFactory.CreateFromMethod(() => { }); var result = new FunctionResult(myInternalFunction); - Console.WriteLine($"Running {nameof(this.NoInputWithFunctionResult)} -> No input -> result: {result.GetType().Name}"); + this._output.WriteLine($"Running {nameof(this.NoInputWithFunctionResult)} -> No input -> result: {result.GetType().Name}"); return result; } @@ -166,7 +183,7 @@ public class LocalExamplePlugin public async Task NoInputTaskWithFunctionResult(Kernel kernel) { var result = await kernel.InvokeAsync(kernel.Plugins["Examples"][nameof(this.NoInputWithVoidResult)]); - Console.WriteLine($"Running {nameof(this.NoInputTaskWithFunctionResult)} -> Injected kernel -> result: {result.GetType().Name}"); + this._output.WriteLine($"Running {nameof(this.NoInputTaskWithFunctionResult)} -> Injected kernel -> result: {result.GetType().Name}"); return result; } @@ -178,7 +195,7 @@ public class LocalExamplePlugin public async Task TaskInjectingKernelWithInputTextAndStringResult(Kernel kernel, string textToSummarize) { var summary = await kernel.InvokeAsync(kernel.Plugins["SummarizePlugin"]["Summarize"], new() { ["input"] = textToSummarize }); - Console.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected kernel + input: [textToSummarize: {textToSummarize[..15]}...{textToSummarize[^15..]}] -> result: {summary}"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected kernel + input: [textToSummarize: {textToSummarize[..15]}...{textToSummarize[^15..]}] -> result: {summary}"); return summary!; } @@ -189,7 +206,7 @@ public class LocalExamplePlugin public async Task TaskInjectingKernelFunctionWithStringResult(KernelFunction executingFunction) { var result = $"Name: {executingFunction.Name}, Description: {executingFunction.Description}"; - Console.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Function -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Function -> result: {result}"); return result; } @@ -200,7 +217,7 @@ public class LocalExamplePlugin public Task TaskInjectingLoggerWithNoResult(ILogger logger) { logger.LogWarning("Running {FunctionName} -> Injected Logger", nameof(this.TaskInjectingLoggerWithNoResult)); - Console.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Logger"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Logger"); return Task.CompletedTask; } @@ -214,7 +231,7 @@ public class LocalExamplePlugin .CreateLogger() .LogWarning("Running {FunctionName} -> Injected Logger", nameof(this.TaskInjectingLoggerWithNoResult)); - Console.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Logger"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Logger"); return Task.CompletedTask; } @@ -231,7 +248,7 @@ public class LocalExamplePlugin } var result = chatMessageContent?.Content; - Console.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Kernel, KernelFunction, KernelArguments, Service Selector -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingKernelWithInputTextAndStringResult)} -> Injected Kernel, KernelFunction, KernelArguments, Service Selector -> result: {result}"); return result ?? string.Empty; } @@ -242,7 +259,7 @@ public class LocalExamplePlugin public async Task TaskInjectingCultureInfoOrIFormatProviderWithStringResult(CultureInfo cultureInfo, IFormatProvider formatProvider) { var result = $"Culture Name: {cultureInfo.Name}, FormatProvider Equals CultureInfo?: {formatProvider.Equals(cultureInfo)}"; - Console.WriteLine($"Running {nameof(this.TaskInjectingCultureInfoOrIFormatProviderWithStringResult)} -> Injected CultureInfo, IFormatProvider -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingCultureInfoOrIFormatProviderWithStringResult)} -> Injected CultureInfo, IFormatProvider -> result: {result}"); return result; } @@ -253,7 +270,7 @@ public class LocalExamplePlugin public async Task TaskInjectingCancellationTokenWithStringResult(CancellationToken cancellationToken) { var result = $"Cancellation resquested: {cancellationToken.IsCancellationRequested}"; - Console.WriteLine($"Running {nameof(this.TaskInjectingCultureInfoOrIFormatProviderWithStringResult)} -> Injected Cancellation Token -> result: {result}"); + this._output.WriteLine($"Running {nameof(this.TaskInjectingCultureInfoOrIFormatProviderWithStringResult)} -> Injected Cancellation Token -> result: {result}"); return result; } diff --git a/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs b/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs index 95bda73557..6ddf492d89 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example10_DescribeAllPluginsAndFunctions.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.IO; using System.Threading.Tasks; using Microsoft.SemanticKernel; @@ -8,15 +7,20 @@ using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Plugins.Core; using Plugins; using RepoUtils; +using Xunit; +using Xunit.Abstractions; -public static class Example10_DescribeAllPluginsAndFunctions +namespace Examples; + +public class Example10_DescribeAllPluginsAndFunctions : BaseTest { /// /// Print a list of all the functions imported into the kernel, including function descriptions, /// list of parameters, parameters descriptions, etc. /// See the end of the file for a sample of what the output looks like. /// - public static Task RunAsync() + [Fact] + public Task RunAsync() { var kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( @@ -46,10 +50,10 @@ public static class Example10_DescribeAllPluginsAndFunctions var functions = kernel.Plugins.GetFunctionsMetadata(); - Console.WriteLine("**********************************************"); - Console.WriteLine("****** Registered plugins and functions ******"); - Console.WriteLine("**********************************************"); - Console.WriteLine(); + WriteLine("**********************************************"); + WriteLine("****** Registered plugins and functions ******"); + WriteLine("**********************************************"); + WriteLine(); foreach (KernelFunctionMetadata func in functions) { @@ -59,22 +63,26 @@ public static class Example10_DescribeAllPluginsAndFunctions return Task.CompletedTask; } - private static void PrintFunction(KernelFunctionMetadata func) + private void PrintFunction(KernelFunctionMetadata func) { - Console.WriteLine($"Plugin: {func.PluginName}"); - Console.WriteLine($" {func.Name}: {func.Description}"); + WriteLine($"Plugin: {func.PluginName}"); + WriteLine($" {func.Name}: {func.Description}"); if (func.Parameters.Count > 0) { - Console.WriteLine(" Params:"); + WriteLine(" Params:"); foreach (var p in func.Parameters) { - Console.WriteLine($" - {p.Name}: {p.Description}"); - Console.WriteLine($" default: '{p.DefaultValue}'"); + WriteLine($" - {p.Name}: {p.Description}"); + WriteLine($" default: '{p.DefaultValue}'"); } } - Console.WriteLine(); + WriteLine(); + } + + public Example10_DescribeAllPluginsAndFunctions(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs b/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs index 018e7a4e81..b84fcf69c0 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example11_WebSearchQueries.cs @@ -1,15 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.Web; +using Xunit; +using Xunit.Abstractions; -public static class Example11_WebSearchQueries +namespace Examples; + +public class Example11_WebSearchQueries : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== WebSearchQueries ========"); + WriteLine("======== WebSearchQueries ========"); Kernel kernel = new(); @@ -20,8 +24,8 @@ public static class Example11_WebSearchQueries var ask = "What's the tallest building in Europe?"; var result = await kernel.InvokeAsync(bing["BingSearchUrl"], new() { ["query"] = ask }); - Console.WriteLine(ask + "\n"); - Console.WriteLine(result.GetValue()); + WriteLine(ask + "\n"); + WriteLine(result.GetValue()); /* Expected output: * ======== WebSearchQueries ======== @@ -31,4 +35,8 @@ public static class Example11_WebSearchQueries * == DONE == */ } + + public Example11_WebSearchQueries(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs b/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs index 595d9ef9e7..bb1cc2b807 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example13_ConversationSummaryPlugin.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.Core; +using xRetry; +using Xunit.Abstractions; -internal static class Example13_ConversationSummaryPlugin +namespace Examples; + +public class Example13_ConversationSummaryPlugin : BaseTest { private const string ChatTranscript = @" @@ -115,16 +118,17 @@ John: You're welcome. I'm glad we could help. Goodbye! Jane: Goodbye! "; - public static async Task RunAsync() + [RetryFact(typeof(HttpOperationException))] + public async Task RunAsync() { await ConversationSummaryPluginAsync(); await GetConversationActionItemsAsync(); await GetConversationTopicsAsync(); } - private static async Task ConversationSummaryPluginAsync() + private async Task ConversationSummaryPluginAsync() { - Console.WriteLine("======== SamplePlugins - Conversation Summary Plugin - Summarize ========"); + WriteLine("======== SamplePlugins - Conversation Summary Plugin - Summarize ========"); Kernel kernel = InitializeKernel(); KernelPlugin conversationSummaryPlugin = kernel.ImportPluginFromType(); @@ -132,13 +136,13 @@ Jane: Goodbye! FunctionResult summary = await kernel.InvokeAsync( conversationSummaryPlugin["SummarizeConversation"], new() { ["input"] = ChatTranscript }); - Console.WriteLine("Generated Summary:"); - Console.WriteLine(summary.GetValue()); + WriteLine("Generated Summary:"); + WriteLine(summary.GetValue()); } - private static async Task GetConversationActionItemsAsync() + private async Task GetConversationActionItemsAsync() { - Console.WriteLine("======== SamplePlugins - Conversation Summary Plugin - Action Items ========"); + WriteLine("======== SamplePlugins - Conversation Summary Plugin - Action Items ========"); Kernel kernel = InitializeKernel(); KernelPlugin conversationSummary = kernel.ImportPluginFromType(); @@ -146,13 +150,13 @@ Jane: Goodbye! FunctionResult summary = await kernel.InvokeAsync( conversationSummary["GetConversationActionItems"], new() { ["input"] = ChatTranscript }); - Console.WriteLine("Generated Action Items:"); - Console.WriteLine(summary.GetValue()); + WriteLine("Generated Action Items:"); + WriteLine(summary.GetValue()); } - private static async Task GetConversationTopicsAsync() + private async Task GetConversationTopicsAsync() { - Console.WriteLine("======== SamplePlugins - Conversation Summary Plugin - Topics ========"); + WriteLine("======== SamplePlugins - Conversation Summary Plugin - Topics ========"); Kernel kernel = InitializeKernel(); KernelPlugin conversationSummary = kernel.ImportPluginFromType(); @@ -160,11 +164,11 @@ Jane: Goodbye! FunctionResult summary = await kernel.InvokeAsync( conversationSummary["GetConversationTopics"], new() { ["input"] = ChatTranscript }); - Console.WriteLine("Generated Topics:"); - Console.WriteLine(summary.GetValue()); + WriteLine("Generated Topics:"); + WriteLine(summary.GetValue()); } - private static Kernel InitializeKernel() + private Kernel InitializeKernel() { Kernel kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( @@ -176,6 +180,10 @@ Jane: Goodbye! return kernel; } + + public Example13_ConversationSummaryPlugin(ITestOutputHelper output) : base(output) + { + } } /* Example Output: diff --git a/dotnet/samples/KernelSyntaxExamples/Example14_SemanticMemory.cs b/dotnet/samples/KernelSyntaxExamples/Example14_SemanticMemory.cs index 8812bc1abc..dc5b52d1ee 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example14_SemanticMemory.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example14_SemanticMemory.cs @@ -6,6 +6,10 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel.Connectors.AzureAISearch; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Memory; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /* The files contains two examples about SK Semantic Memory. * @@ -15,15 +19,16 @@ using Microsoft.SemanticKernel.Memory; * Semantic Memory allows to store your data like traditional DBs, * adding the ability to query it using natural language. */ -public static class Example14_SemanticMemory +public class Example14_SemanticMemory : BaseTest { private const string MemoryCollectionName = "SKGitHub"; - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("=============================================================="); - Console.WriteLine("======== Semantic Memory using Azure AI Search ========"); - Console.WriteLine("=============================================================="); + WriteLine("=============================================================="); + WriteLine("======== Semantic Memory using Azure AI Search ========"); + WriteLine("=============================================================="); /* This example leverages Azure AI Search to provide SK with Semantic Memory. * @@ -38,9 +43,9 @@ public static class Example14_SemanticMemory await RunExampleAsync(memoryWithACS); - Console.WriteLine("===================================================="); - Console.WriteLine("======== Semantic Memory (volatile, in RAM) ========"); - Console.WriteLine("===================================================="); + WriteLine("===================================================="); + WriteLine("======== Semantic Memory (volatile, in RAM) ========"); + WriteLine("===================================================="); /* You can build your own semantic memory combining an Embedding Generator * with a Memory storage that supports search by similarity (ie semantic search). @@ -59,7 +64,7 @@ public static class Example14_SemanticMemory await RunExampleAsync(memoryWithCustomDb); } - public static async Task RunExampleAsync(ISemanticTextMemory memory) + private async Task RunExampleAsync(ISemanticTextMemory memory) { await StoreMemoryAsync(memory); @@ -98,26 +103,26 @@ public static class Example14_SemanticMemory */ } - private static async Task SearchMemoryAsync(ISemanticTextMemory memory, string query) + private async Task SearchMemoryAsync(ISemanticTextMemory memory, string query) { - Console.WriteLine("\nQuery: " + query + "\n"); + WriteLine("\nQuery: " + query + "\n"); var memoryResults = memory.SearchAsync(MemoryCollectionName, query, limit: 2, minRelevanceScore: 0.5); int i = 0; await foreach (MemoryQueryResult memoryResult in memoryResults) { - Console.WriteLine($"Result {++i}:"); - Console.WriteLine(" URL: : " + memoryResult.Metadata.Id); - Console.WriteLine(" Title : " + memoryResult.Metadata.Description); - Console.WriteLine(" Relevance: " + memoryResult.Relevance); - Console.WriteLine(); + WriteLine($"Result {++i}:"); + WriteLine(" URL: : " + memoryResult.Metadata.Id); + WriteLine(" Title : " + memoryResult.Metadata.Description); + WriteLine(" Relevance: " + memoryResult.Relevance); + WriteLine(); } - Console.WriteLine("----------------------"); + WriteLine("----------------------"); } - private static async Task StoreMemoryAsync(ISemanticTextMemory memory) + private async Task StoreMemoryAsync(ISemanticTextMemory memory) { /* Store some data in the semantic memory. * @@ -127,7 +132,7 @@ public static class Example14_SemanticMemory * care of creating and storing the index */ - Console.WriteLine("\nAdding some GitHub file URLs and their descriptions to the semantic memory."); + WriteLine("\nAdding some GitHub file URLs and their descriptions to the semantic memory."); var githubFiles = SampleData(); var i = 0; foreach (var entry in githubFiles) @@ -142,7 +147,7 @@ public static class Example14_SemanticMemory Console.Write($" #{++i} saved."); } - Console.WriteLine("\n----------------------"); + WriteLine("\n----------------------"); } private static Dictionary SampleData() @@ -161,4 +166,8 @@ public static class Example14_SemanticMemory = "C# class that defines a volatile embedding store", }; } + + public Example14_SemanticMemory(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs b/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs index 80638f1c42..801032dfe8 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example15_TextMemoryPlugin.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureAISearch; @@ -21,12 +19,19 @@ using Microsoft.SemanticKernel.Plugins.Memory; using Npgsql; using RepoUtils; using StackExchange.Redis; +using Xunit; +using Xunit.Abstractions; -public static class Example15_TextMemoryPlugin +namespace Examples; + +public class Example15_TextMemoryPlugin : BaseTest { private const string MemoryCollectionName = "aboutMe"; - public static async Task RunAsync(CancellationToken cancellationToken = default) + [Theory] + [InlineData("Volatile")] + [InlineData("AzureAISearch")] + public async Task RunAsync(string provider) { IMemoryStore store; @@ -35,7 +40,11 @@ public static class Example15_TextMemoryPlugin /////////////////////////////////////////////////////////////////////////////////////////// // Volatile Memory Store - an in-memory store that is not persisted - store = new VolatileMemoryStore(); + switch (provider) + { + case "AzureAISearch": store = CreateSampleAzureAISearchMemoryStore(); break; + default: store = new VolatileMemoryStore(); break; + } // Sqlite Memory Store - a file-based store that persists data in a Sqlite database // store = await CreateSampleSqliteMemoryStoreAsync(); @@ -70,58 +79,58 @@ public static class Example15_TextMemoryPlugin // Kusto Memory Store // store = CreateSampleKustoMemoryStore(); - await RunWithStoreAsync(store, cancellationToken); + await RunWithStoreAsync(store); } - private static async Task CreateSampleSqliteMemoryStoreAsync() + private async Task CreateSampleSqliteMemoryStoreAsync() { IMemoryStore store = await SqliteMemoryStore.ConnectAsync("memories.sqlite"); return store; } - private static async Task CreateSampleDuckDbMemoryStoreAsync() + private async Task CreateSampleDuckDbMemoryStoreAsync() { IMemoryStore store = await DuckDBMemoryStore.ConnectAsync("memories.duckdb"); return store; } - private static IMemoryStore CreateSampleMongoDBMemoryStore() + private IMemoryStore CreateSampleMongoDBMemoryStore() { IMemoryStore store = new MongoDBMemoryStore(TestConfiguration.MongoDB.ConnectionString, "memoryPluginExample"); return store; } - private static IMemoryStore CreateSampleAzureAISearchMemoryStore() + private IMemoryStore CreateSampleAzureAISearchMemoryStore() { IMemoryStore store = new AzureAISearchMemoryStore(TestConfiguration.AzureAISearch.Endpoint, TestConfiguration.AzureAISearch.ApiKey); return store; } - private static IMemoryStore CreateSampleChromaMemoryStore() + private IMemoryStore CreateSampleChromaMemoryStore() { IMemoryStore store = new ChromaMemoryStore(TestConfiguration.Chroma.Endpoint, ConsoleLogger.LoggerFactory); return store; } - private static IMemoryStore CreateSampleQdrantMemoryStore() + private IMemoryStore CreateSampleQdrantMemoryStore() { IMemoryStore store = new QdrantMemoryStore(TestConfiguration.Qdrant.Endpoint, 1536, ConsoleLogger.LoggerFactory); return store; } - private static IMemoryStore CreateSamplePineconeMemoryStore() + private IMemoryStore CreateSamplePineconeMemoryStore() { IMemoryStore store = new PineconeMemoryStore(TestConfiguration.Pinecone.Environment, TestConfiguration.Pinecone.ApiKey, ConsoleLogger.LoggerFactory); return store; } - private static IMemoryStore CreateSampleWeaviateMemoryStore() + private IMemoryStore CreateSampleWeaviateMemoryStore() { IMemoryStore store = new WeaviateMemoryStore(TestConfiguration.Weaviate.Endpoint, TestConfiguration.Weaviate.ApiKey); return store; } - private static async Task CreateSampleRedisMemoryStoreAsync() + private async Task CreateSampleRedisMemoryStoreAsync() { string configuration = TestConfiguration.Redis.Configuration; ConnectionMultiplexer connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configuration); @@ -146,7 +155,7 @@ public static class Example15_TextMemoryPlugin return store; } - private static async Task RunWithStoreAsync(IMemoryStore memoryStore, CancellationToken cancellationToken) + private async Task RunWithStoreAsync(IMemoryStore memoryStore) { var kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) @@ -165,25 +174,25 @@ public static class Example15_TextMemoryPlugin // // This is a simple way to store memories from a code perspective, without using the Kernel. ///////////////////////////////////////////////////////////////////////////////////////////////////// - Console.WriteLine("== PART 1a: Saving Memories through the ISemanticTextMemory object =="); + WriteLine("== PART 1a: Saving Memories through the ISemanticTextMemory object =="); - Console.WriteLine("Saving memory with key 'info1': \"My name is Andrea\""); - await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info1", text: "My name is Andrea", cancellationToken: cancellationToken); + WriteLine("Saving memory with key 'info1': \"My name is Andrea\""); + await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info1", text: "My name is Andrea"); - Console.WriteLine("Saving memory with key 'info2': \"I work as a tourist operator\""); - await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info2", text: "I work as a tourist operator", cancellationToken: cancellationToken); + WriteLine("Saving memory with key 'info2': \"I work as a tourist operator\""); + await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info2", text: "I work as a tourist operator"); - Console.WriteLine("Saving memory with key 'info3': \"I've been living in Seattle since 2005\""); - await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info3", text: "I've been living in Seattle since 2005", cancellationToken: cancellationToken); + WriteLine("Saving memory with key 'info3': \"I've been living in Seattle since 2005\""); + await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info3", text: "I've been living in Seattle since 2005"); - Console.WriteLine("Saving memory with key 'info4': \"I visited France and Italy five times since 2015\""); - await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info4", text: "I visited France and Italy five times since 2015", cancellationToken: cancellationToken); + WriteLine("Saving memory with key 'info4': \"I visited France and Italy five times since 2015\""); + await textMemory.SaveInformationAsync(MemoryCollectionName, id: "info4", text: "I visited France and Italy five times since 2015"); // Retrieve a memory - Console.WriteLine("== PART 1b: Retrieving Memories through the ISemanticTextMemory object =="); - MemoryQueryResult? lookup = await textMemory.GetAsync(MemoryCollectionName, "info1", cancellationToken: cancellationToken); - Console.WriteLine("Memory with key 'info1':" + lookup?.Metadata.Text ?? "ERROR: memory not found"); - Console.WriteLine(); + WriteLine("== PART 1b: Retrieving Memories through the ISemanticTextMemory object =="); + MemoryQueryResult? lookup = await textMemory.GetAsync(MemoryCollectionName, "info1"); + WriteLine("Memory with key 'info1':" + lookup?.Metadata.Text ?? "ERROR: memory not found"); + WriteLine(); ///////////////////////////////////////////////////////////////////////////////////////////////////// // PART 2: Create TextMemoryPlugin, store and retrieve memories through the Kernel. @@ -191,30 +200,30 @@ public static class Example15_TextMemoryPlugin // This enables prompt functions and the AI (via Planners) to access memories ///////////////////////////////////////////////////////////////////////////////////////////////////// - Console.WriteLine("== PART 2a: Saving Memories through the Kernel with TextMemoryPlugin and the 'Save' function =="); + WriteLine("== PART 2a: Saving Memories through the Kernel with TextMemoryPlugin and the 'Save' function =="); // Import the TextMemoryPlugin into the Kernel for other functions var memoryPlugin = kernel.ImportPluginFromObject(new TextMemoryPlugin(textMemory)); // Save a memory with the Kernel - Console.WriteLine("Saving memory with key 'info5': \"My family is from New York\""); + WriteLine("Saving memory with key 'info5': \"My family is from New York\""); await kernel.InvokeAsync(memoryPlugin["Save"], new() { [TextMemoryPlugin.InputParam] = "My family is from New York", [TextMemoryPlugin.CollectionParam] = MemoryCollectionName, [TextMemoryPlugin.KeyParam] = "info5", - }, cancellationToken); + }); // Retrieve a specific memory with the Kernel - Console.WriteLine("== PART 2b: Retrieving Memories through the Kernel with TextMemoryPlugin and the 'Retrieve' function =="); + WriteLine("== PART 2b: Retrieving Memories through the Kernel with TextMemoryPlugin and the 'Retrieve' function =="); var result = await kernel.InvokeAsync(memoryPlugin["Retrieve"], new KernelArguments() { [TextMemoryPlugin.CollectionParam] = MemoryCollectionName, [TextMemoryPlugin.KeyParam] = "info5" - }, cancellationToken); + }); - Console.WriteLine("Memory with key 'info5':" + result.GetValue() ?? "ERROR: memory not found"); - Console.WriteLine(); + WriteLine("Memory with key 'info5':" + result.GetValue() ?? "ERROR: memory not found"); + WriteLine(); ///////////////////////////////////////////////////////////////////////////////////////////////////// // PART 3: Recall similar ideas with semantic search @@ -222,24 +231,23 @@ public static class Example15_TextMemoryPlugin // Uses AI Embeddings for fuzzy lookup of memories based on intent, rather than a specific key. ///////////////////////////////////////////////////////////////////////////////////////////////////// - Console.WriteLine("== PART 3: Recall (similarity search) with AI Embeddings =="); + WriteLine("== PART 3: Recall (similarity search) with AI Embeddings =="); - Console.WriteLine("== PART 3a: Recall (similarity search) with ISemanticTextMemory =="); - Console.WriteLine("Ask: where did I grow up?"); + WriteLine("== PART 3a: Recall (similarity search) with ISemanticTextMemory =="); + WriteLine("Ask: where did I grow up?"); await foreach (var answer in textMemory.SearchAsync( collection: MemoryCollectionName, query: "where did I grow up?", limit: 2, minRelevanceScore: 0.79, - withEmbeddings: true, - cancellationToken: cancellationToken)) + withEmbeddings: true)) { - Console.WriteLine($"Answer: {answer.Metadata.Text}"); + WriteLine($"Answer: {answer.Metadata.Text}"); } - Console.WriteLine("== PART 3b: Recall (similarity search) with Kernel and TextMemoryPlugin 'Recall' function =="); - Console.WriteLine("Ask: where do I live?"); + WriteLine("== PART 3b: Recall (similarity search) with Kernel and TextMemoryPlugin 'Recall' function =="); + WriteLine("Ask: where do I live?"); result = await kernel.InvokeAsync(memoryPlugin["Recall"], new() { @@ -247,10 +255,10 @@ public static class Example15_TextMemoryPlugin [TextMemoryPlugin.CollectionParam] = MemoryCollectionName, [TextMemoryPlugin.LimitParam] = "2", [TextMemoryPlugin.RelevanceParam] = "0.79", - }, cancellationToken); + }); - Console.WriteLine($"Answer: {result.GetValue()}"); - Console.WriteLine(); + WriteLine($"Answer: {result.GetValue()}"); + WriteLine(); /* Output: @@ -271,7 +279,7 @@ public static class Example15_TextMemoryPlugin // the text generation model to answer a natural language query. ///////////////////////////////////////////////////////////////////////////////////////////////////// - Console.WriteLine("== PART 4: Using TextMemoryPlugin 'Recall' function in a Prompt Function =="); + WriteLine("== PART 4: Using TextMemoryPlugin 'Recall' function in a Prompt Function =="); // Build a prompt function that uses memory to find facts const string RecallFunctionDefinition = @" @@ -295,10 +303,10 @@ Answer: [TextMemoryPlugin.CollectionParam] = MemoryCollectionName, [TextMemoryPlugin.LimitParam] = "2", [TextMemoryPlugin.RelevanceParam] = "0.79", - }, cancellationToken); + }); - Console.WriteLine("Ask: Do I live in the same town where I grew up?"); - Console.WriteLine($"Answer: {result.GetValue()}"); + WriteLine("Ask: Do I live in the same town where I grew up?"); + WriteLine($"Answer: {result.GetValue()}"); /* Approximate Output: @@ -310,25 +318,29 @@ Answer: // ///////////////////////////////////////////////////////////////////////////////////////////////////// - Console.WriteLine("== PART 5: Cleanup, deleting database collection =="); + WriteLine("== PART 5: Cleanup, deleting database collection =="); - Console.WriteLine("Printing Collections in DB..."); - var collections = memoryStore.GetCollectionsAsync(cancellationToken); + WriteLine("Printing Collections in DB..."); + var collections = memoryStore.GetCollectionsAsync(); await foreach (var collection in collections) { - Console.WriteLine(collection); + WriteLine(collection); } - Console.WriteLine(); + WriteLine(); - Console.WriteLine("Removing Collection {0}", MemoryCollectionName); - await memoryStore.DeleteCollectionAsync(MemoryCollectionName, cancellationToken); - Console.WriteLine(); + WriteLine($"Removing Collection {MemoryCollectionName}"); + await memoryStore.DeleteCollectionAsync(MemoryCollectionName); + WriteLine(); - Console.WriteLine($"Printing Collections in DB (after removing {MemoryCollectionName})..."); - collections = memoryStore.GetCollectionsAsync(cancellationToken); + WriteLine($"Printing Collections in DB (after removing {MemoryCollectionName})..."); + collections = memoryStore.GetCollectionsAsync(); await foreach (var collection in collections) { - Console.WriteLine(collection); + WriteLine(collection); } } + + public Example15_TextMemoryPlugin(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs b/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs index f51a5ed200..7cb61dd0b9 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example16_CustomLLM.cs @@ -8,6 +8,10 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.TextGeneration; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /** * The following example shows how to plug a custom text generation model into SK. @@ -24,18 +28,12 @@ using Microsoft.SemanticKernel.TextGeneration; * * Refer to example 33 for streaming chat completion. */ -public static class Example16_CustomLLM +public class Example16_CustomLLM : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task CustomTextGenerationWithKernelFunctionAsync() { - await CustomTextGenerationWithKernelFunctionAsync(); - await CustomTextGenerationAsync(); - await CustomTextGenerationStreamAsync(); - } - - private static async Task CustomTextGenerationWithKernelFunctionAsync() - { - Console.WriteLine("\n======== Custom LLM - Text Completion - KernelFunction ========"); + WriteLine("\n======== Custom LLM - Text Completion - KernelFunction ========"); IKernelBuilder builder = Kernel.CreateBuilder(); // Add your text generation service as a singleton instance @@ -48,39 +46,41 @@ public static class Example16_CustomLLM var paragraphWritingFunction = kernel.CreateFunctionFromPrompt(FunctionDefinition); const string Input = "Why AI is awesome"; - Console.WriteLine($"Function input: {Input}\n"); + WriteLine($"Function input: {Input}\n"); var result = await paragraphWritingFunction.InvokeAsync(kernel, new() { ["input"] = Input }); - Console.WriteLine(result); + WriteLine(result); } - private static async Task CustomTextGenerationAsync() + [Fact] + public async Task CustomTextGenerationAsync() { - Console.WriteLine("\n======== Custom LLM - Text Completion - Raw ========"); + WriteLine("\n======== Custom LLM - Text Completion - Raw ========"); const string Prompt = "Write one paragraph on why AI is awesome."; var completionService = new MyTextGenerationService(); - Console.WriteLine($"Prompt: {Prompt}\n"); + WriteLine($"Prompt: {Prompt}\n"); var result = await completionService.GetTextContentAsync(Prompt); - Console.WriteLine(result); + WriteLine(result); } - private static async Task CustomTextGenerationStreamAsync() + [Fact] + public async Task CustomTextGenerationStreamAsync() { - Console.WriteLine("\n======== Custom LLM - Text Completion - Raw Streaming ========"); + WriteLine("\n======== Custom LLM - Text Completion - Raw Streaming ========"); const string Prompt = "Write one paragraph on why AI is awesome."; var completionService = new MyTextGenerationService(); - Console.WriteLine($"Prompt: {Prompt}\n"); + WriteLine($"Prompt: {Prompt}\n"); await foreach (var message in completionService.GetStreamingTextContentsAsync(Prompt)) { - Console.Write(message); + Write(message); } - Console.WriteLine(); + WriteLine(); } /// @@ -117,4 +117,8 @@ providing personalized recommendations, entertainment, and assistance. AI is awe }); } } + + public Example16_CustomLLM(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example17_ChatGPT.cs b/dotnet/samples/KernelSyntaxExamples/Example17_ChatGPT.cs index 3a50d09734..3115e2f499 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example17_ChatGPT.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example17_ChatGPT.cs @@ -1,18 +1,25 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with OpenAI ChatGPT API -public static class Example17_ChatGPT +public class Example17_ChatGPT : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task OpenAIChatSampleAsync() { - await AzureOpenAIChatSampleAsync(); - await OpenAIChatSampleAsync(); + WriteLine("======== Open AI - ChatGPT ========"); + + OpenAIChatCompletionService chatCompletionService = new(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); + + await StartChatAsync(chatCompletionService); /* Output: @@ -43,18 +50,10 @@ public static class Example17_ChatGPT */ } - private static async Task OpenAIChatSampleAsync() + [Fact] + public async Task AzureOpenAIChatSampleAsync() { - Console.WriteLine("======== Open AI - ChatGPT ========"); - - OpenAIChatCompletionService chatCompletionService = new(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); - - await StartChatAsync(chatCompletionService); - } - - private static async Task AzureOpenAIChatSampleAsync() - { - Console.WriteLine("======== Azure Open AI - ChatGPT ========"); + WriteLine("======== Azure Open AI - ChatGPT ========"); AzureOpenAIChatCompletionService chatCompletionService = new( deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, @@ -65,10 +64,10 @@ public static class Example17_ChatGPT await StartChatAsync(chatCompletionService); } - private static async Task StartChatAsync(IChatCompletionService chatGPT) + private async Task StartChatAsync(IChatCompletionService chatGPT) { - Console.WriteLine("Chat content:"); - Console.WriteLine("------------------------"); + WriteLine("Chat content:"); + WriteLine("------------------------"); var chatHistory = new ChatHistory("You are a librarian, expert about books"); @@ -94,13 +93,17 @@ public static class Example17_ChatGPT /// /// Outputs the last message of the chat history /// - private static Task MessageOutputAsync(ChatHistory chatHistory) + private Task MessageOutputAsync(ChatHistory chatHistory) { var message = chatHistory.Last(); - Console.WriteLine($"{message.Role}: {message.Content}"); - Console.WriteLine("------------------------"); + WriteLine($"{message.Role}: {message.Content}"); + WriteLine("------------------------"); return Task.CompletedTask; } + + public Example17_ChatGPT(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example18_DallE.cs b/dotnet/samples/KernelSyntaxExamples/Example18_DallE.cs index 85e7e0860f..a78e891d41 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example18_DallE.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example18_DallE.cs @@ -7,19 +7,18 @@ using Microsoft.Extensions.Http.Resilience; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.TextToImage; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with OpenAI Dall-E 2 to create images -public static class Example18_DallE +public class Example18_DallE : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task OpenAIDallEAsync() { - await OpenAIDallEAsync(); - await AzureOpenAIDallEAsync(); - } - - private static async Task OpenAIDallEAsync() - { - Console.WriteLine("======== OpenAI Dall-E 2 Text To Image ========"); + WriteLine("======== OpenAI Dall-E 2 Text To Image ========"); Kernel kernel = Kernel.CreateBuilder() .AddOpenAITextToImage(TestConfiguration.OpenAI.ApiKey) // Add your text to image service @@ -31,8 +30,8 @@ public static class Example18_DallE var imageDescription = "A cute baby sea otter"; var image = await dallE.GenerateImageAsync(imageDescription, 256, 256); - Console.WriteLine(imageDescription); - Console.WriteLine("Image URL: " + image); + WriteLine(imageDescription); + WriteLine("Image URL: " + image); /* Output: @@ -41,7 +40,7 @@ public static class Example18_DallE */ - Console.WriteLine("======== Chat with images ========"); + WriteLine("======== Chat with images ========"); var chatGPT = kernel.GetRequiredService(); var chatHistory = new ChatHistory( @@ -52,23 +51,23 @@ public static class Example18_DallE var msg = "Hi, I'm from Tokyo, where are you from?"; chatHistory.AddUserMessage(msg); - Console.WriteLine("User: " + msg); + WriteLine("User: " + msg); var reply = await chatGPT.GetChatMessageContentAsync(chatHistory); chatHistory.Add(reply); image = await dallE.GenerateImageAsync(reply.Content!, 256, 256); - Console.WriteLine("Bot: " + image); - Console.WriteLine("Img description: " + reply); + WriteLine("Bot: " + image); + WriteLine("Img description: " + reply); msg = "Oh, wow. Not sure where that is, could you provide more details?"; chatHistory.AddUserMessage(msg); - Console.WriteLine("User: " + msg); + WriteLine("User: " + msg); reply = await chatGPT.GetChatMessageContentAsync(chatHistory); chatHistory.Add(reply); image = await dallE.GenerateImageAsync(reply.Content!, 256, 256); - Console.WriteLine("Bot: " + image); - Console.WriteLine("Img description: " + reply); + WriteLine("Bot: " + image); + WriteLine("Img description: " + reply); /* Output: @@ -83,9 +82,10 @@ public static class Example18_DallE */ } - public static async Task AzureOpenAIDallEAsync() + [Fact(Skip = "Generating the Image can take too long and often break the test")] + public async Task AzureOpenAIDallEAsync() { - Console.WriteLine("========Azure OpenAI Dall-E 3 Text To Image ========"); + WriteLine("========Azure OpenAI Dall-E 3 Text To Image ========"); var builder = Kernel.CreateBuilder() .AddAzureOpenAITextToImage( // Add your text to image service @@ -105,6 +105,7 @@ public static class Example18_DallE c.AddStandardResilienceHandler().Configure(o => { o.Retry.MaxRetryAttempts = 5; + o.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60); }); }); @@ -114,8 +115,8 @@ public static class Example18_DallE var imageDescription = "A cute baby sea otter"; var image = await dallE.GenerateImageAsync(imageDescription, 1024, 1024); - Console.WriteLine(imageDescription); - Console.WriteLine("Image URL: " + image); + WriteLine(imageDescription); + WriteLine("Image URL: " + image); /* Output: @@ -124,7 +125,7 @@ public static class Example18_DallE */ - Console.WriteLine("======== Chat with images ========"); + WriteLine("======== Chat with images ========"); var chatGPT = kernel.GetRequiredService(); var chatHistory = new ChatHistory( @@ -135,23 +136,23 @@ public static class Example18_DallE var msg = "Hi, I'm from Tokyo, where are you from?"; chatHistory.AddUserMessage(msg); - Console.WriteLine("User: " + msg); + WriteLine("User: " + msg); var reply = await chatGPT.GetChatMessageContentAsync(chatHistory); chatHistory.Add(reply); image = await dallE.GenerateImageAsync(reply.Content!, 1024, 1024); - Console.WriteLine("Bot: " + image); - Console.WriteLine("Img description: " + reply); + WriteLine("Bot: " + image); + WriteLine("Img description: " + reply); msg = "Oh, wow. Not sure where that is, could you provide more details?"; chatHistory.AddUserMessage(msg); - Console.WriteLine("User: " + msg); + WriteLine("User: " + msg); reply = await chatGPT.GetChatMessageContentAsync(chatHistory); chatHistory.Add(reply); image = await dallE.GenerateImageAsync(reply.Content!, 1024, 1024); - Console.WriteLine("Bot: " + image); - Console.WriteLine("Img description: " + reply); + WriteLine("Bot: " + image); + WriteLine("Img description: " + reply); /* Output: @@ -165,4 +166,8 @@ public static class Example18_DallE */ } + + public Example18_DallE(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example20_HuggingFace.cs b/dotnet/samples/KernelSyntaxExamples/Example20_HuggingFace.cs index d773e8fe68..6e1ce7ec47 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example20_HuggingFace.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example20_HuggingFace.cs @@ -1,25 +1,23 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with HuggingFace API. -public static class Example20_HuggingFace +public class Example20_HuggingFace : BaseTest { - public static async Task RunAsync() - { - await RunInferenceApiExampleAsync(); - await RunLlamaExampleAsync(); - } - /// /// This example uses HuggingFace Inference API to access hosted models. /// More information here: /// - private static async Task RunInferenceApiExampleAsync() + [Fact] + public async Task RunInferenceApiExampleAsync() { - Console.WriteLine("\n======== HuggingFace Inference API example ========\n"); + WriteLine("\n======== HuggingFace Inference API example ========\n"); Kernel kernel = Kernel.CreateBuilder() .AddHuggingFaceTextGeneration( @@ -31,7 +29,7 @@ public static class Example20_HuggingFace var result = await kernel.InvokeAsync(questionAnswerFunction, new() { ["input"] = "What is New York?" }); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); } /// @@ -45,20 +43,21 @@ public static class Example20_HuggingFace /// Note: Your Hugging Face account email address MUST match the email you provide on the Meta website, or your request will not be approved. /// /// - private static async Task RunLlamaExampleAsync() + [Fact(Skip = "Requires local model or Huggingface Pro subscription")] + public async Task RunLlamaExampleAsync() { - Console.WriteLine("\n======== HuggingFace Llama 2 example ========\n"); + WriteLine("\n======== HuggingFace Llama 2 example ========\n"); // HuggingFace Llama 2 model: https://huggingface.co/meta-llama/Llama-2-7b-hf const string Model = "meta-llama/Llama-2-7b-hf"; // HuggingFace local HTTP server endpoint - const string Endpoint = "http://localhost:5000/completions"; + // const string Endpoint = "http://localhost:5000/completions"; Kernel kernel = Kernel.CreateBuilder() .AddHuggingFaceTextGeneration( model: Model, - endpoint: Endpoint, + //endpoint: Endpoint, apiKey: TestConfiguration.HuggingFace.ApiKey) .Build(); @@ -66,6 +65,10 @@ public static class Example20_HuggingFace var result = await kernel.InvokeAsync(questionAnswerFunction, new() { ["input"] = "What is New York?" }); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); + } + + public Example20_HuggingFace(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example21_OpenAIPlugins.cs b/dotnet/samples/KernelSyntaxExamples/Example21_OpenAIPlugins.cs index 9481a0721a..5f0c7a1d68 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example21_OpenAIPlugins.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example21_OpenAIPlugins.cs @@ -5,22 +5,18 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.OpenApi; +using Xunit; +using Xunit.Abstractions; -public static class Example21_OpenAIPlugins +namespace Examples; + +public class Example21_OpenAIPlugins : BaseTest { - public static Task RunAsync() - { - // Uncomment after filling the template below - // await RunOpenAIPluginAsync(); - - // --------------- Example using Klarna's OpenAI plugin ------------------------ - return CallKlarnaAsync(); - } - /// /// Generic template on how to call OpenAI plugins /// - public static async Task RunOpenAIPluginAsync() + [Fact(Skip = "Run it only after filling the template below")] + public async Task RunOpenAIPluginAsync() { Kernel kernel = new(); @@ -38,10 +34,11 @@ public static class Example21_OpenAIPlugins var result = functionResult.GetValue(); - Console.WriteLine("Function execution result: {0}", result?.Content?.ToString()); + WriteLine($"Function execution result: {result?.Content}"); } - public static async Task CallKlarnaAsync() + [Fact] + public async Task CallKlarnaAsync() { Kernel kernel = new(); @@ -58,6 +55,10 @@ public static class Example21_OpenAIPlugins var result = functionResult.GetValue(); - Console.WriteLine("Function execution result: {0}", result?.Content?.ToString()); + WriteLine($"Function execution result: {result?.Content}"); + } + + public Example21_OpenAIPlugins(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example22_OpenAIPlugin_AzureKeyVault.cs b/dotnet/samples/KernelSyntaxExamples/Example22_OpenAIPlugin_AzureKeyVault.cs index 21a1997cd1..14e914a9e2 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example22_OpenAIPlugin_AzureKeyVault.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example22_OpenAIPlugin_AzureKeyVault.cs @@ -13,8 +13,12 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.OpenApi; using Resources; +using Xunit; +using Xunit.Abstractions; -public static class Example22_OpenAIPlugin_AzureKeyVault +namespace Examples; + +public class Example22_OpenAIPlugin_AzureKeyVault : BaseTest { private const string SecretName = "Foo"; private const string SecretValue = "Bar"; @@ -38,7 +42,8 @@ public static class Example22_OpenAIPlugin_AzureKeyVault /// /// 5. Replace your tenant ID with the "TENANT_ID" placeholder in dotnet/samples/KernelSyntaxExamples/Resources/22-ai-plugin.json /// - public static async Task RunAsync() + [Fact(Skip = "Setup credentials")] + public async Task RunAsync() { var authenticationProvider = new OpenAIAuthenticationProvider( new Dictionary>() @@ -78,7 +83,7 @@ public static class Example22_OpenAIPlugin_AzureKeyVault await GetSecretFromAzureKeyVaultWithRetryAsync(kernel, plugin); } - public static async Task AddSecretToAzureKeyVaultAsync(Kernel kernel, KernelPlugin plugin) + private async Task AddSecretToAzureKeyVaultAsync(Kernel kernel, KernelPlugin plugin) { // Add arguments for required parameters, arguments for optional ones can be skipped. var arguments = new KernelArguments @@ -97,7 +102,7 @@ public static class Example22_OpenAIPlugin_AzureKeyVault Console.WriteLine("SetSecret function result: {0}", result?.Content?.ToString()); } - public static async Task GetSecretFromAzureKeyVaultWithRetryAsync(Kernel kernel, KernelPlugin plugin) + private static async Task GetSecretFromAzureKeyVaultWithRetryAsync(Kernel kernel, KernelPlugin plugin) { // Add arguments for required parameters, arguments for optional ones can be skipped. var arguments = new KernelArguments(); @@ -111,6 +116,10 @@ public static class Example22_OpenAIPlugin_AzureKeyVault Console.WriteLine("GetSecret function result: {0}", result?.Content?.ToString()); } + + public Example22_OpenAIPlugin_AzureKeyVault(ITestOutputHelper output) : base(output) + { + } } #region Utility Classes diff --git a/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs b/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs index 58a4bb0836..c484d04072 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs @@ -12,8 +12,12 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.OpenApi; +using Xunit; +using Xunit.Abstractions; -public static class Example24_OpenApiPlugin_Jira +namespace Examples; + +public class Example24_OpenApiPlugin_Jira : BaseTest { private static readonly JsonSerializerOptions s_jsonOptionsCache = new() { @@ -37,7 +41,8 @@ public static class Example24_OpenApiPlugin_Jira /// instance then select "Manage account". /// 4. Configure the secrets as described by the ReadMe.md in the dotnet/samples/KernelSyntaxExamples folder. /// - public static async Task RunAsync() + [Fact(Skip = "Setup credentials")] + public async Task RunAsync() { Kernel kernel = new(); @@ -90,10 +95,10 @@ public static class Example24_OpenApiPlugin_Jira // Run operation via the semantic kernel var result = await kernel.InvokeAsync(jiraFunctions["GetIssue"], arguments); - Console.WriteLine("\n\n\n"); + WriteLine("\n\n\n"); var formattedContent = JsonSerializer.Serialize( result.GetValue(), s_jsonOptionsCache); - Console.WriteLine("GetIssue jiraPlugin response: \n{0}", formattedContent); + WriteLine($"GetIssue jiraPlugin response: \n{formattedContent}"); // AddComment Function arguments["issueKey"] = "TEST-2"; @@ -102,10 +107,10 @@ public static class Example24_OpenApiPlugin_Jira // Run operation via the semantic kernel result = await kernel.InvokeAsync(jiraFunctions["AddComment"], arguments); - Console.WriteLine("\n\n\n"); + WriteLine("\n\n\n"); formattedContent = JsonSerializer.Serialize(result.GetValue(), s_jsonOptionsCache); - Console.WriteLine("AddComment jiraPlugin response: \n{0}", formattedContent); + WriteLine($"AddComment jiraPlugin response: \n{formattedContent}"); } #region Example of authentication providers @@ -251,4 +256,8 @@ public static class Example24_OpenApiPlugin_Jira } #endregion + + public Example24_OpenApiPlugin_Jira(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example25_ReadOnlyMemoryStore.cs b/dotnet/samples/KernelSyntaxExamples/Example25_ReadOnlyMemoryStore.cs index 4d1cdcd7c3..9c54af7e75 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example25_ReadOnlyMemoryStore.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example25_ReadOnlyMemoryStore.cs @@ -10,6 +10,10 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.Memory; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// This sample provides a custom implementation of that is read only. @@ -18,26 +22,27 @@ using Microsoft.SemanticKernel.Memory; /// of has a single collection, and thus does not need to be named. /// It also assumes that the JSON formatted data can be deserialized into objects. /// -public static class Example25_ReadOnlyMemoryStore +public class Example25_ReadOnlyMemoryStore : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { var store = new ReadOnlyMemoryStore(s_jsonVectorEntries); var embedding = new ReadOnlyMemory(new float[] { 22, 4, 6 }); - Console.WriteLine("Reading data from custom read-only memory store"); + WriteLine("Reading data from custom read-only memory store"); var memoryRecord = await store.GetAsync("collection", "key3"); if (memoryRecord != null) { - Console.WriteLine("ID = {0}, Embedding = {1}", memoryRecord.Metadata.Id, string.Join(", ", MemoryMarshal.ToEnumerable(memoryRecord.Embedding))); + WriteLine($"ID = {memoryRecord.Metadata.Id}, Embedding = {string.Join(", ", MemoryMarshal.ToEnumerable(memoryRecord.Embedding))}"); } - Console.WriteLine("Getting most similar vector to {0}", string.Join(", ", MemoryMarshal.ToEnumerable(embedding))); + WriteLine($"Getting most similar vector to {string.Join(", ", MemoryMarshal.ToEnumerable(embedding))}"); var result = await store.GetNearestMatchAsync("collection", embedding, 0.0); if (result.HasValue) { - Console.WriteLine("Embedding = {0}, Similarity = {1}", string.Join(", ", MemoryMarshal.ToEnumerable(result.Value.Item1.Embedding)), result.Value.Item2); + WriteLine($"ID = {string.Join(", ", MemoryMarshal.ToEnumerable(result.Value.Item1.Embedding))}, Embedding = {result.Value.Item2}"); } } @@ -236,4 +241,8 @@ public static class Example25_ReadOnlyMemoryStore ""timestamp"": null } ]"; + + public Example25_ReadOnlyMemoryStore(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example26_AADAuth.cs b/dotnet/samples/KernelSyntaxExamples/Example26_AADAuth.cs index 39819ff3a4..3ad939e5f5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example26_AADAuth.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example26_AADAuth.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Azure.Identity; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// This example shows how to connect your app to Azure OpenAI using @@ -18,11 +21,12 @@ using Microsoft.SemanticKernel.ChatCompletion; /// -Shared tokens /// -etc. /// -public static class Example26_AADAuth +public class Example26_AADAuth : BaseTest { - public static async Task RunAsync() + [Fact(Skip = "Setup credentials")] + public async Task RunAsync() { - Console.WriteLine("======== SK with AAD Auth ========"); + WriteLine("======== SK with AAD Auth ========"); // Optional: choose which authentication to support var authOptions = new DefaultAzureCredentialOptions @@ -55,8 +59,12 @@ public static class Example26_AADAuth // Bot reply var reply = await chatGPT.GetChatMessageContentAsync(chatHistory); - Console.WriteLine(reply); + WriteLine(reply); /* Output: Why did the hourglass go to the doctor? Because it was feeling a little run down! */ } + + public Example26_AADAuth(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example27_PromptFunctionsUsingChatGPT.cs b/dotnet/samples/KernelSyntaxExamples/Example27_PromptFunctionsUsingChatGPT.cs index 76cd5a17d6..d2b83da3f5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example27_PromptFunctionsUsingChatGPT.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example27_PromptFunctionsUsingChatGPT.cs @@ -1,17 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// This example shows how to use GPT3.5 Chat model for prompts and prompt functions. /// -public static class Example27_PromptFunctionsUsingChatGPT +public class Example27_PromptFunctionsUsingChatGPT : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Using Chat GPT model for text generation ========"); + WriteLine("======== Using Chat GPT model for text generation ========"); Kernel kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( @@ -25,7 +29,7 @@ public static class Example27_PromptFunctionsUsingChatGPT "List the two planets closest to '{{$input}}', excluding moons, using bullet points."); var result = await func.InvokeAsync(kernel, new() { ["input"] = "Jupiter" }); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); /* Output: @@ -33,4 +37,8 @@ public static class Example27_PromptFunctionsUsingChatGPT - Uranus */ } + + public Example27_PromptFunctionsUsingChatGPT(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example30_ChatWithPrompts.cs b/dotnet/samples/KernelSyntaxExamples/Example30_ChatWithPrompts.cs index 38453b613e..f82940dad5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example30_ChatWithPrompts.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example30_ChatWithPrompts.cs @@ -7,6 +7,10 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Plugins.Core; using Resources; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// Scenario: @@ -31,11 +35,12 @@ using Resources; /// Out of scope and not in the example: if needed, one could go further and use a semantic /// function (with extra cost) asking AI to generate the text to send to the Chat model. /// -public static class Example30_ChatWithPrompts +public class Example30_ChatWithPrompts : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Chat with prompts ========"); + WriteLine("======== Chat with prompts ========"); /* Load 3 files: * - 30-system-prompt.txt: the system prompt, used to initialize the chat session. @@ -76,12 +81,12 @@ public static class Example30_ChatWithPrompts // Render the system prompt. This string is used to configure the chat. // This contains the context, ie a piece of a wikipedia page selected by the user. string systemMessage = await promptTemplateFactory.Create(new PromptTemplateConfig(systemPromptTemplate)).RenderAsync(kernel, arguments); - Console.WriteLine($"------------------------------------\n{systemMessage}"); + WriteLine($"------------------------------------\n{systemMessage}"); // Render the user prompt. This string is the query sent by the user // This contains the user request, ie "extract locations as a bullet point list" string userMessage = await promptTemplateFactory.Create(new PromptTemplateConfig(userPromptTemplate)).RenderAsync(kernel, arguments); - Console.WriteLine($"------------------------------------\n{userMessage}"); + WriteLine($"------------------------------------\n{userMessage}"); // Client used to request answers var chatCompletion = kernel.GetRequiredService(); @@ -96,7 +101,7 @@ public static class Example30_ChatWithPrompts // Finally, get the response from AI var answer = await chatCompletion.GetChatMessageContentAsync(chatHistory); - Console.WriteLine($"------------------------------------\n{answer}"); + WriteLine($"------------------------------------\n{answer}"); /* @@ -121,4 +126,8 @@ public static class Example30_ChatWithPrompts */ } + + public Example30_ChatWithPrompts(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example32_StreamingCompletion.cs b/dotnet/samples/KernelSyntaxExamples/Example32_StreamingCompletion.cs index 9422182d6d..7adb053467 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example32_StreamingCompletion.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example32_StreamingCompletion.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.TextGeneration; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /** * The following example shows how to use Semantic Kernel with streaming text completion. @@ -15,17 +18,12 @@ using Microsoft.SemanticKernel.TextGeneration; * * Refer to example 33 for streaming chat completion. */ -public static class Example32_StreamingCompletion +public class Example32_StreamingCompletion : BaseTest { - public static async Task RunAsync() + [Fact] + public Task AzureOpenAITextGenerationStreamAsync() { - await AzureOpenAITextGenerationStreamAsync(); - await OpenAITextGenerationStreamAsync(); - } - - private static async Task AzureOpenAITextGenerationStreamAsync() - { - Console.WriteLine("======== Azure OpenAI - Text Completion - Raw Streaming ========"); + WriteLine("======== Azure OpenAI - Text Completion - Raw Streaming ========"); var textGeneration = new AzureOpenAITextGenerationService( deploymentName: TestConfiguration.AzureOpenAI.DeploymentName, @@ -33,19 +31,20 @@ public static class Example32_StreamingCompletion apiKey: TestConfiguration.AzureOpenAI.ApiKey, modelId: TestConfiguration.AzureOpenAI.ModelId); - await TextGenerationStreamAsync(textGeneration); + return this.TextGenerationStreamAsync(textGeneration); } - private static async Task OpenAITextGenerationStreamAsync() + [Fact] + public Task OpenAITextGenerationStreamAsync() { - Console.WriteLine("======== Open AI - Text Completion - Raw Streaming ========"); + WriteLine("======== Open AI - Text Completion - Raw Streaming ========"); - var textGeneration = new OpenAITextGenerationService("text-davinci-003", TestConfiguration.OpenAI.ApiKey); + var textGeneration = new OpenAITextGenerationService("gpt-3.5-turbo-instruct", TestConfiguration.OpenAI.ApiKey); - await TextGenerationStreamAsync(textGeneration); + return this.TextGenerationStreamAsync(textGeneration); } - private static async Task TextGenerationStreamAsync(ITextGenerationService textGeneration) + private async Task TextGenerationStreamAsync(ITextGenerationService textGeneration) { var executionSettings = new OpenAIPromptExecutionSettings() { @@ -58,12 +57,16 @@ public static class Example32_StreamingCompletion var prompt = "Write one paragraph why AI is awesome"; - Console.WriteLine("Prompt: " + prompt); + WriteLine("Prompt: " + prompt); await foreach (var content in textGeneration.GetStreamingTextContentsAsync(prompt, executionSettings)) { - Console.Write(content); + Write(content); } - Console.WriteLine(); + WriteLine(); + } + + public Example32_StreamingCompletion(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example33_StreamingChat.cs b/dotnet/samples/KernelSyntaxExamples/Example33_StreamingChat.cs index 913f32ed43..1b0223e36f 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example33_StreamingChat.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example33_StreamingChat.cs @@ -1,32 +1,31 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with streaming Chat Completion -public static class Example33_StreamingChat +public class Example33_StreamingChat : BaseTest { - public static async Task RunAsync() + [Fact] + public Task OpenAIChatStreamSampleAsync() { - await AzureOpenAIChatStreamSampleAsync(); - await OpenAIChatStreamSampleAsync(); - } - - private static async Task OpenAIChatStreamSampleAsync() - { - Console.WriteLine("======== Open AI - ChatGPT Streaming ========"); + WriteLine("======== Open AI - ChatGPT Streaming ========"); OpenAIChatCompletionService chatCompletionService = new(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); - await StartStreamingChatAsync(chatCompletionService); + return this.StartStreamingChatAsync(chatCompletionService); } - private static async Task AzureOpenAIChatStreamSampleAsync() + [Fact] + public Task AzureOpenAIChatStreamSampleAsync() { - Console.WriteLine("======== Azure Open AI - ChatGPT Streaming ========"); + WriteLine("======== Azure Open AI - ChatGPT Streaming ========"); AzureOpenAIChatCompletionService chatCompletionService = new( deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, @@ -34,13 +33,13 @@ public static class Example33_StreamingChat apiKey: TestConfiguration.AzureOpenAI.ApiKey, modelId: TestConfiguration.AzureOpenAI.ChatModelId); - await StartStreamingChatAsync(chatCompletionService); + return this.StartStreamingChatAsync(chatCompletionService); } - private static async Task StartStreamingChatAsync(IChatCompletionService chatCompletionService) + private async Task StartStreamingChatAsync(IChatCompletionService chatCompletionService) { - Console.WriteLine("Chat content:"); - Console.WriteLine("------------------------"); + WriteLine("Chat content:"); + WriteLine("------------------------"); var chatHistory = new ChatHistory("You are a librarian, expert about books"); await MessageOutputAsync(chatHistory); @@ -60,7 +59,7 @@ public static class Example33_StreamingChat await StreamMessageOutputAsync(chatCompletionService, chatHistory, AuthorRole.Assistant); } - private static async Task StreamMessageOutputAsync(IChatCompletionService chatCompletionService, ChatHistory chatHistory, AuthorRole authorRole) + private async Task StreamMessageOutputAsync(IChatCompletionService chatCompletionService, ChatHistory chatHistory, AuthorRole authorRole) { bool roleWritten = false; string fullMessage = string.Empty; @@ -69,31 +68,35 @@ public static class Example33_StreamingChat { if (!roleWritten && chatUpdate.Role.HasValue) { - Console.Write($"{chatUpdate.Role.Value}: {chatUpdate.Content}"); + Write($"{chatUpdate.Role.Value}: {chatUpdate.Content}"); roleWritten = true; } if (chatUpdate.Content is { Length: > 0 }) { fullMessage += chatUpdate.Content; - Console.Write(chatUpdate.Content); + Write(chatUpdate.Content); } } - Console.WriteLine("\n------------------------"); + WriteLine("\n------------------------"); chatHistory.AddMessage(authorRole, fullMessage); } /// /// Outputs the last message of the chat history /// - private static Task MessageOutputAsync(ChatHistory chatHistory) + private Task MessageOutputAsync(ChatHistory chatHistory) { var message = chatHistory.Last(); - Console.WriteLine($"{message.Role}: {message.Content}"); - Console.WriteLine("------------------------"); + WriteLine($"{message.Role}: {message.Content}"); + WriteLine("------------------------"); return Task.CompletedTask; } + + public Example33_StreamingChat(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example35_GrpcPlugins.cs b/dotnet/samples/KernelSyntaxExamples/Example35_GrpcPlugins.cs index b536b6fd77..f9d8ed41d7 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example35_GrpcPlugins.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example35_GrpcPlugins.cs @@ -1,14 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.Grpc; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use gRPC plugins. -public static class Example35_GrpcPlugins +public class Example35_GrpcPlugins : BaseTest { - public static async Task RunAsync() + [Fact(Skip = "Setup crendentials")] + public async Task RunAsync() { Kernel kernel = new(); @@ -25,6 +29,10 @@ public static class Example35_GrpcPlugins // Run var result = await kernel.InvokeAsync(plugin[""], arguments); - Console.WriteLine("Plugin response: {0}", result.GetValue()); + WriteLine($"Plugin response: {result.GetValue()}"); + } + + public Example35_GrpcPlugins(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example36_MultiCompletion.cs b/dotnet/samples/KernelSyntaxExamples/Example36_MultiCompletion.cs index eb15323619..486ebb5859 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example36_MultiCompletion.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example36_MultiCompletion.cs @@ -1,22 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with streaming Multiple Results Chat Completion. -public static class Example36_MultiCompletion +public class Example36_MultiCompletion : BaseTest { - public static async Task RunAsync() + [Fact] + public Task AzureOpenAIMultiChatCompletionAsync() { - await AzureOpenAIMultiChatCompletionAsync(); - await OpenAIMultiChatCompletionAsync(); - } - - private static async Task AzureOpenAIMultiChatCompletionAsync() - { - Console.WriteLine("======== Azure OpenAI - Multiple Chat Completion ========"); + WriteLine("======== Azure OpenAI - Multiple Chat Completion ========"); var chatCompletionService = new AzureOpenAIChatCompletionService( deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, @@ -24,21 +22,22 @@ public static class Example36_MultiCompletion apiKey: TestConfiguration.AzureOpenAI.ApiKey, modelId: TestConfiguration.AzureOpenAI.ChatModelId); - await ChatCompletionAsync(chatCompletionService); + return ChatCompletionAsync(chatCompletionService); } - private static async Task OpenAIMultiChatCompletionAsync() + [Fact] + public Task OpenAIMultiChatCompletionAsync() { - Console.WriteLine("======== Open AI - Multiple Chat Completion ========"); + WriteLine("======== Open AI - Multiple Chat Completion ========"); var chatCompletionService = new OpenAIChatCompletionService( TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); - await ChatCompletionAsync(chatCompletionService); + return ChatCompletionAsync(chatCompletionService); } - private static async Task ChatCompletionAsync(IChatCompletionService chatCompletionService) + private async Task ChatCompletionAsync(IChatCompletionService chatCompletionService) { var executionSettings = new OpenAIPromptExecutionSettings() { @@ -55,10 +54,14 @@ public static class Example36_MultiCompletion foreach (var chatMessageChoice in await chatCompletionService.GetChatMessageContentsAsync(chatHistory, executionSettings)) { - Console.Write(chatMessageChoice.Content); - Console.WriteLine("\n-------------\n"); + Write(chatMessageChoice.Content ?? string.Empty); + WriteLine("\n-------------\n"); } - Console.WriteLine(); + WriteLine(); + } + + public Example36_MultiCompletion(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs b/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs index aee50b034e..15e4f120f5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example40_DIContainer.cs @@ -6,14 +6,19 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using RepoUtils; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following examples show how to use SK SDK in applications using DI/IoC containers. -public static class Example40_DIContainer +public class Example40_DIContainer : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { var collection = new ServiceCollection(); - collection.AddSingleton(ConsoleLogger.LoggerFactory); + collection.AddSingleton(ConsoleLogger.LoggerFactory); collection.AddOpenAITextGeneration(TestConfiguration.OpenAI.ModelId, TestConfiguration.OpenAI.ApiKey); collection.AddSingleton(); @@ -56,4 +61,8 @@ public static class Example40_DIContainer this._logger.LogWarning("Result - {0}", result.GetValue()); } } + + public Example40_DIContainer(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs b/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs index afdfb4f829..2b11a19c56 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example41_HttpClientUsage.cs @@ -2,32 +2,21 @@ using System; using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // These examples show how to use HttpClient and HttpClientFactory within SK SDK. -public static class Example41_HttpClientUsage +public class Example41_HttpClientUsage : BaseTest { - public static Task RunAsync() - { - //Examples showing how to use HttpClient. - UseDefaultHttpClient(); - - UseCustomHttpClient(); - - //Examples showing how to use HttpClientFactory. - UseBasicRegistrationWithHttpClientFactory(); - - UseNamedRegistrationWitHttpClientFactory(); - - return Task.CompletedTask; - } - /// /// Demonstrates the usage of the default HttpClient provided by the SK SDK. /// - private static void UseDefaultHttpClient() + [Fact] + public void UseDefaultHttpClient() { var kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( @@ -39,7 +28,8 @@ public static class Example41_HttpClientUsage /// /// Demonstrates the usage of a custom HttpClient. /// - private static void UseCustomHttpClient() + [Fact] + public void UseCustomHttpClient() { using var httpClient = new HttpClient(); @@ -55,7 +45,8 @@ public static class Example41_HttpClientUsage /// /// Demonstrates the "basic usage" approach for HttpClientFactory. /// - private static void UseBasicRegistrationWithHttpClientFactory() + [Fact] + public void UseBasicRegistrationWithHttpClientFactory() { //More details - https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#basic-usage var serviceCollection = new ServiceCollection(); @@ -77,7 +68,8 @@ public static class Example41_HttpClientUsage /// /// Demonstrates the "named clients" approach for HttpClientFactory. /// - private static void UseNamedRegistrationWitHttpClientFactory() + [Fact] + public void UseNamedRegistrationWitHttpClientFactory() { // More details https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#named-clients @@ -102,4 +94,8 @@ public static class Example41_HttpClientUsage .Build(); }); } + + public Example41_HttpClientUsage(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs index ed371bf9a9..eb006df2b0 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example42_KernelBuilder.cs @@ -5,51 +5,62 @@ // You can access the builder using Kernel.CreateBuilder(). using System.Diagnostics; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Plugins.Core; +using Xunit; +using Xunit.Abstractions; -public static class Example42_KernelBuilder +namespace Examples; + +public class Example42_KernelBuilder : BaseTest { - public static Task RunAsync() + [Fact] + public void BuildKernelWithAzureChatCompletion() { - string azureOpenAIKey = TestConfiguration.AzureOpenAI.ApiKey; - string azureOpenAIEndpoint = TestConfiguration.AzureOpenAI.Endpoint; - string azureOpenAIChatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string azureOpenAIChatModelId = TestConfiguration.AzureOpenAI.ChatModelId; - // KernelBuilder provides a simple way to configure a Kernel. This constructs a kernel // with logging and an Azure OpenAI chat completion service configured. Kernel kernel1 = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIChatDeploymentName, - endpoint: azureOpenAIEndpoint, - apiKey: azureOpenAIKey, - modelId: azureOpenAIChatModelId) + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId) .Build(); + } + [Fact] + public void BuildKernelUsingServiceCollection() + { // For greater flexibility and to incorporate arbitrary services, KernelBuilder.Services // provides direct access to an underlying IServiceCollection. IKernelBuilder builder = Kernel.CreateBuilder(); builder.Services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)) - .AddHttpClient() - .AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIChatDeploymentName, - endpoint: azureOpenAIEndpoint, - apiKey: azureOpenAIKey, - modelId: azureOpenAIChatModelId); + .AddHttpClient() + .AddAzureOpenAIChatCompletion( + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId); Kernel kernel2 = builder.Build(); + } + [Fact] + public void BuildKernelWithPlugins() + { // Plugins may also be configured via the corresponding Plugins property. - builder = Kernel.CreateBuilder(); + var builder = Kernel.CreateBuilder(); builder.Plugins.AddFromType(); Kernel kernel3 = builder.Build(); + } + [Fact] + public void BuildKernelUsingServiceProvider() + { // Every call to KernelBuilder.Build creates a new Kernel instance, with a new service provider // and a new plugin collection. - builder = Kernel.CreateBuilder(); + var builder = Kernel.CreateBuilder(); Debug.Assert(!ReferenceEquals(builder.Build(), builder.Build())); // KernelBuilder provides a convenient API for creating Kernel instances. However, it is just a @@ -59,32 +70,38 @@ public static class Example42_KernelBuilder services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)); services.AddHttpClient(); services.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIChatDeploymentName, - endpoint: azureOpenAIEndpoint, - apiKey: azureOpenAIKey, - modelId: azureOpenAIChatModelId); + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId); Kernel kernel4 = new(services.BuildServiceProvider()); // Kernels can also be constructed and resolved via such a dependency injection container. services.AddTransient(); Kernel kernel5 = services.BuildServiceProvider().GetRequiredService(); + } + [Fact] + public void BuildKernelUsingServiceCollectionExtension() + { // In fact, the AddKernel method exists to simplify this, registering a singleton KernelPluginCollection // that can be populated automatically with all IKernelPlugins registered in the collection, and a // transient Kernel that can then automatically be constructed from the service provider and resulting // plugins collection. - services = new ServiceCollection(); + var services = new ServiceCollection(); services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)); services.AddHttpClient(); services.AddKernel().AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIChatDeploymentName, - endpoint: azureOpenAIEndpoint, - apiKey: azureOpenAIKey, - modelId: azureOpenAIChatModelId); + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId); services.AddSingleton(sp => KernelPluginFactory.CreateFromType(serviceProvider: sp)); services.AddSingleton(sp => KernelPluginFactory.CreateFromType(serviceProvider: sp)); Kernel kernel6 = services.BuildServiceProvider().GetRequiredService(); + } - return Task.CompletedTask; + public Example42_KernelBuilder(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example43_GetModelResult.cs b/dotnet/samples/KernelSyntaxExamples/Example43_GetModelResult.cs index 5ebf5a3832..123454987a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example43_GetModelResult.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example43_GetModelResult.cs @@ -1,15 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using RepoUtils; +using Xunit; +using Xunit.Abstractions; -public static class Example43_GetModelResult +namespace Examples; + +public class Example43_GetModelResult : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task GetTokenUsageMetadataAsync() { - Console.WriteLine("======== Inline Function Definition + Invocation ========"); + WriteLine("======== Inline Function Definition + Invocation ========"); // Create kernel Kernel kernel = Kernel.CreateBuilder() @@ -26,8 +30,12 @@ public static class Example43_GetModelResult FunctionResult result = await kernel.InvokeAsync(myFunction, new() { ["input"] = "travel" }); // Display results - Console.WriteLine(result.GetValue()); - Console.WriteLine(result.Metadata?["Usage"]?.AsJson()); - Console.WriteLine(); + WriteLine(result.GetValue()); + WriteLine(result.Metadata?["Usage"]?.AsJson()); + WriteLine(); + } + + public Example43_GetModelResult(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example44_MultiChatCompletion.cs b/dotnet/samples/KernelSyntaxExamples/Example44_MultiChatCompletion.cs index 9c0c9ff2d4..c54347fbf1 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example44_MultiChatCompletion.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example44_MultiChatCompletion.cs @@ -1,23 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with Multiple Results Text Completion as streaming -public static class Example44_MultiChatCompletion +public class Example44_MultiChatCompletion : BaseTest { - public static async Task RunAsync() + [Fact] + public Task AzureOpenAIMultiChatCompletionAsync() { - await AzureOpenAIMultiChatCompletionAsync(); - await OpenAIMultiChatCompletionAsync(); - } - - private static async Task AzureOpenAIMultiChatCompletionAsync() - { - Console.WriteLine("======== Azure OpenAI - Multiple Chat Completion ========"); + WriteLine("======== Azure OpenAI - Multiple Chat Completion ========"); AzureOpenAIChatCompletionService chatCompletionService = new( deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, @@ -25,19 +23,20 @@ public static class Example44_MultiChatCompletion apiKey: TestConfiguration.AzureOpenAI.ApiKey, modelId: TestConfiguration.AzureOpenAI.ChatModelId); - await RunChatAsync(chatCompletionService); + return RunChatAsync(chatCompletionService); } - private static async Task OpenAIMultiChatCompletionAsync() + [Fact] + public Task OpenAIMultiChatCompletionAsync() { - Console.WriteLine("======== Open AI - Multiple Chat Completion ========"); + WriteLine("======== Open AI - Multiple Chat Completion ========"); OpenAIChatCompletionService chatCompletionService = new(modelId: TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); - await RunChatAsync(chatCompletionService); + return RunChatAsync(chatCompletionService); } - private static async Task RunChatAsync(IChatCompletionService chatCompletionService) + private async Task RunChatAsync(IChatCompletionService chatCompletionService) { var chatHistory = new ChatHistory("You are a librarian, expert about books"); @@ -61,19 +60,23 @@ public static class Example44_MultiChatCompletion await MessageOutputAsync(chatHistory); } - Console.WriteLine(); + WriteLine(); } /// /// Outputs the last message of the chat history /// - private static Task MessageOutputAsync(ChatHistory chatHistory) + private Task MessageOutputAsync(ChatHistory chatHistory) { var message = chatHistory.Last(); - Console.WriteLine($"{message.Role}: {message.Content}"); - Console.WriteLine("------------------------"); + WriteLine($"{message.Role}: {message.Content}"); + WriteLine("------------------------"); return Task.CompletedTask; } + + public Example44_MultiChatCompletion(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example45_MultiStreamingChatCompletion.cs b/dotnet/samples/KernelSyntaxExamples/Example45_MultiStreamingChatCompletion.cs index e3602d7a20..b510839b48 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example45_MultiStreamingChatCompletion.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example45_MultiStreamingChatCompletion.cs @@ -6,21 +6,18 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // The following example shows how to use Semantic Kernel with multiple streaming chat completion results. -public static class Example45_MultiStreamingChatCompletion +public class Example45_MultiStreamingChatCompletion : BaseTest { - // First procress multiple Azure OpenAI chat completions - // and then multiple plain OpenAI chat completions. - public static async Task RunAsync() + [Fact] + public Task AzureOpenAIMultiStreamingChatCompletionAsync() { - await AzureOpenAIMultiStreamingChatCompletionAsync(); - await OpenAIMultiStreamingChatCompletionAsync(); - } - - private static async Task AzureOpenAIMultiStreamingChatCompletionAsync() - { - Console.WriteLine("======== Azure OpenAI - Multiple Chat Completions - Raw Streaming ========"); + WriteLine("======== Azure OpenAI - Multiple Chat Completions - Raw Streaming ========"); AzureOpenAIChatCompletionService chatCompletionService = new( deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, @@ -28,18 +25,19 @@ public static class Example45_MultiStreamingChatCompletion apiKey: TestConfiguration.AzureOpenAI.ApiKey, modelId: TestConfiguration.AzureOpenAI.ChatModelId); - await StreamingChatCompletionAsync(chatCompletionService, 3); + return StreamingChatCompletionAsync(chatCompletionService, 3); } - private static async Task OpenAIMultiStreamingChatCompletionAsync() + [Fact] + public Task OpenAIMultiStreamingChatCompletionAsync() { - Console.WriteLine("======== OpenAI - Multiple Chat Completions - Raw Streaming ========"); + WriteLine("======== OpenAI - Multiple Chat Completions - Raw Streaming ========"); OpenAIChatCompletionService chatCompletionService = new( modelId: TestConfiguration.OpenAI.ChatModelId, apiKey: TestConfiguration.OpenAI.ApiKey); - await StreamingChatCompletionAsync(chatCompletionService, 3); + return StreamingChatCompletionAsync(chatCompletionService, 3); } /// @@ -47,7 +45,7 @@ public static class Example45_MultiStreamingChatCompletion /// /// Chat completion service to use /// Number of results to get for each chat completion request - private static async Task StreamingChatCompletionAsync(IChatCompletionService chatCompletionService, + private async Task StreamingChatCompletionAsync(IChatCompletionService chatCompletionService, int numResultsPerPrompt) { var executionSettings = new OpenAIPromptExecutionSettings() @@ -62,24 +60,25 @@ public static class Example45_MultiStreamingChatCompletion var consoleLinesPerResult = 10; - ClearDisplayByAddingEmptyLines(); + // Uncomment this if you want to use a console app to display the results + // ClearDisplayByAddingEmptyLines(); var prompt = "Hi, I'm looking for 5 random title names for sci-fi books"; await ProcessStreamAsyncEnumerableAsync(chatCompletionService, prompt, executionSettings, consoleLinesPerResult); - Console.WriteLine(); + WriteLine(); // Set cursor position to after displayed results - Console.SetCursorPosition(0, executionSettings.ResultsPerPrompt * consoleLinesPerResult); + // Console.SetCursorPosition(0, executionSettings.ResultsPerPrompt * consoleLinesPerResult); - Console.WriteLine(); + WriteLine(); } /// /// Does the actual streaming and display of the chat completion. /// - private static async Task ProcessStreamAsyncEnumerableAsync(IChatCompletionService chatCompletionService, string prompt, + private async Task ProcessStreamAsyncEnumerableAsync(IChatCompletionService chatCompletionService, string prompt, OpenAIPromptExecutionSettings executionSettings, int consoleLinesPerResult) { var messagesPerChoice = new Dictionary(); @@ -90,12 +89,13 @@ public static class Example45_MultiStreamingChatCompletion { // Set cursor position to the beginning of where this choice (i.e. this result of // a single multi-result request) is to be displayed. - Console.SetCursorPosition(0, chatUpdate.ChoiceIndex * consoleLinesPerResult + 1); + // Console.SetCursorPosition(0, chatUpdate.ChoiceIndex * consoleLinesPerResult + 1); // The first time around, start choice text with role information if (!messagesPerChoice.ContainsKey(chatUpdate.ChoiceIndex)) { messagesPerChoice[chatUpdate.ChoiceIndex] = $"Role: {chatUpdate.Role ?? new AuthorRole()}\n"; + Write($"Choice index: {chatUpdate.ChoiceIndex}, Role: {chatUpdate.Role ?? new AuthorRole()}"); } // Add latest completion bit, if any @@ -105,18 +105,30 @@ public static class Example45_MultiStreamingChatCompletion } // Overwrite what is currently in the console area for the updated choice - Console.Write(messagesPerChoice[chatUpdate.ChoiceIndex]); + // Console.Write(messagesPerChoice[chatUpdate.ChoiceIndex]); + Write($"Choice index: {chatUpdate.ChoiceIndex}, Content: {chatUpdate.Content}"); + } + + // Display the aggregated results + foreach (string message in messagesPerChoice.Values) + { + WriteLine("-------------------"); + WriteLine(message); } } /// /// Add enough new lines to clear the console window. /// - private static void ClearDisplayByAddingEmptyLines() + private void ClearDisplayByAddingEmptyLines() { for (int i = 0; i < Console.WindowHeight - 2; i++) { - Console.WriteLine(); + WriteLine(); } } + + public Example45_MultiStreamingChatCompletion(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example48_GroundednessChecks.cs b/dotnet/samples/KernelSyntaxExamples/Example48_GroundednessChecks.cs index 8885ee5152..f57d9a3d17 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example48_GroundednessChecks.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example48_GroundednessChecks.cs @@ -1,15 +1,118 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.IO; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Planning.Handlebars; using Microsoft.SemanticKernel.Plugins.Core; using RepoUtils; +using xRetry; +using Xunit; +using Xunit.Abstractions; -internal static class Example48_GroundednessChecks +namespace Examples; + +public class Example48_GroundednessChecks : BaseTest { + [RetryFact(typeof(HttpOperationException))] + public async Task GroundednessCheckingAsync() + { + WriteLine("\n======== Groundedness Checks ========"); + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion( + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId) + .Build(); + + string folder = RepoFiles.SamplePluginsPath(); + var summarizePlugin = kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin")); + var groundingPlugin = kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "GroundingPlugin")); + + var create_summary = summarizePlugin["Summarize"]; + var entityExtraction = groundingPlugin["ExtractEntities"]; + var reference_check = groundingPlugin["ReferenceCheckEntities"]; + var entity_excision = groundingPlugin["ExciseEntities"]; + + var summaryText = @" +My father, a respected resident of Milan, was a close friend of a merchant named Beaufort who, after a series of +misfortunes, moved to Zurich in poverty. My father was upset by his friend's troubles and sought him out, +finding him in a mean street. Beaufort had saved a small sum of money, but it was not enough to support him and +his daughter, Mary. Mary procured work to eek out a living, but after ten months her father died, leaving +her a beggar. My father came to her aid and two years later they married. +"; + + KernelArguments variables = new() + { + ["input"] = summaryText, + ["topic"] = "people and places", + ["example_entities"] = "John, Jane, mother, brother, Paris, Rome" + }; + + var extractionResult = (await kernel.InvokeAsync(entityExtraction, variables)).ToString(); + + WriteLine("======== Extract Entities ========"); + WriteLine(extractionResult); + + variables["input"] = extractionResult; + variables["reference_context"] = GroundingText; + + var groundingResult = (await kernel.InvokeAsync(reference_check, variables)).ToString(); + + WriteLine("\n======== Reference Check ========"); + WriteLine(groundingResult); + + variables["input"] = summaryText; + variables["ungrounded_entities"] = groundingResult; + var excisionResult = await kernel.InvokeAsync(entity_excision, variables); + + WriteLine("\n======== Excise Entities ========"); + WriteLine(excisionResult.GetValue()); + } + + [Fact(Skip = "Unreliable - Needs attention")] + public async Task PlanningWithGroundednessAsync() + { + var targetTopic = "people and places"; + var samples = "John, Jane, mother, brother, Paris, Rome"; + var ask = @$"Make a summary of the following text. Then make a list of entities +related to {targetTopic} (such as {samples}) which are present in the summary. +Take this list of entities, and from it make another list of those which are not +grounded in the original input text. Finally, rewrite your summary to remove the entities +which are not grounded in the original. + +Text:\n{GroundingText}; +"; + + WriteLine("\n======== Planning - Groundedness Checks ========"); + + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion( + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId) + .Build(); + + string folder = RepoFiles.SamplePluginsPath(); + kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin")); + kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "GroundingPlugin")); + + kernel.ImportPluginFromType(); + + var planner = new HandlebarsPlanner(); + var plan = await planner.CreatePlanAsync(kernel, ask); + + WriteLine($"======== Goal: ========\n{ask}"); + WriteLine($"======== Plan ========\n{plan}"); + + var result = await plan.InvokeAsync(kernel); + + WriteLine("======== Result ========"); + WriteLine(result); + } + private const string GroundingText = @"""I am by birth a Genevese, and my family is one of the most distinguished of that republic. My ancestors had been for many years counsellors and syndics, and my father had filled several public situations with honour and reputation.He was respected by all who knew him for his integrity and indefatigable attention @@ -47,107 +150,8 @@ the chamber. He came like a protecting spirit to the poor girl, who committed he interment of his friend he conducted her to Geneva and placed her under the protection of a relation.Two years after this event Caroline became his wife."""; - public static async Task RunAsync() + public Example48_GroundednessChecks(ITestOutputHelper output) : base(output) { - await GroundednessCheckingAsync(); - await PlanningWithGroundednessAsync(); - } - - public static async Task GroundednessCheckingAsync() - { - Console.WriteLine("\n======== Groundedness Checks ========"); - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion( - deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, - endpoint: TestConfiguration.AzureOpenAI.Endpoint, - apiKey: TestConfiguration.AzureOpenAI.ApiKey, - modelId: TestConfiguration.AzureOpenAI.ChatModelId) - .Build(); - - string folder = RepoFiles.SamplePluginsPath(); - var summarizePlugin = kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin")); - var groundingPlugin = kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "GroundingPlugin")); - - var create_summary = summarizePlugin["Summarize"]; - var entityExtraction = groundingPlugin["ExtractEntities"]; - var reference_check = groundingPlugin["ReferenceCheckEntities"]; - var entity_excision = groundingPlugin["ExciseEntities"]; - - var summaryText = @" -My father, a respected resident of Milan, was a close friend of a merchant named Beaufort who, after a series of -misfortunes, moved to Zurich in poverty. My father was upset by his friend's troubles and sought him out, -finding him in a mean street. Beaufort had saved a small sum of money, but it was not enough to support him and -his daughter, Mary. Mary procured work to eek out a living, but after ten months her father died, leaving -her a beggar. My father came to her aid and two years later they married. -"; - - KernelArguments variables = new() - { - ["input"] = summaryText, - ["topic"] = "people and places", - ["example_entities"] = "John, Jane, mother, brother, Paris, Rome" - }; - - var extractionResult = (await kernel.InvokeAsync(entityExtraction, variables)).ToString(); - - Console.WriteLine("======== Extract Entities ========"); - Console.WriteLine(extractionResult); - - variables["input"] = extractionResult; - variables["reference_context"] = GroundingText; - - var groundingResult = (await kernel.InvokeAsync(reference_check, variables)).ToString(); - - Console.WriteLine("\n======== Reference Check ========"); - Console.WriteLine(groundingResult); - - variables["input"] = summaryText; - variables["ungrounded_entities"] = groundingResult; - var excisionResult = await kernel.InvokeAsync(entity_excision, variables); - - Console.WriteLine("\n======== Excise Entities ========"); - Console.WriteLine(excisionResult.GetValue()); - } - - public static async Task PlanningWithGroundednessAsync() - { - var targetTopic = "people and places"; - var samples = "John, Jane, mother, brother, Paris, Rome"; - var ask = @$"Make a summary of the following text. Then make a list of entities -related to {targetTopic} (such as {samples}) which are present in the summary. -Take this list of entities, and from it make another list of those which are not -grounded in the original input text. Finally, rewrite your summary to remove the entities -which are not grounded in the original. - -Text:\n{GroundingText}; -"; - - Console.WriteLine("\n======== Planning - Groundedness Checks ========"); - - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion( - deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, - endpoint: TestConfiguration.AzureOpenAI.Endpoint, - apiKey: TestConfiguration.AzureOpenAI.ApiKey, - modelId: TestConfiguration.AzureOpenAI.ChatModelId) - .Build(); - - string folder = RepoFiles.SamplePluginsPath(); - kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin")); - kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "GroundingPlugin")); - - kernel.ImportPluginFromType(); - - var planner = new HandlebarsPlanner(); - var plan = await planner.CreatePlanAsync(kernel, ask); - - Console.WriteLine($"======== Goal: ========\n{ask}"); - Console.WriteLine($"======== Plan ========\n{plan}"); - - var result = await plan.InvokeAsync(kernel); - - Console.WriteLine("======== Result ========"); - Console.WriteLine(result); } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example49_LogitBias.cs b/dotnet/samples/KernelSyntaxExamples/Example49_LogitBias.cs index 2a1f786a97..f61b787c8d 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example49_LogitBias.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example49_LogitBias.cs @@ -1,18 +1,22 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /** * Logit_bias is an optional parameter that modifies the likelihood of specified tokens appearing in a Completion. * When using the Token Selection Biases parameter, the bias is added to the logits generated by the model prior to sampling. */ -public static class Example49_LogitBias +public class Example49_LogitBias : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { OpenAIChatCompletionService chatCompletionService = new(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); @@ -23,14 +27,15 @@ public static class Example49_LogitBias // "novel literature reading author library story chapter paperback hardcover ebook publishing fiction nonfiction manuscript textbook bestseller bookstore reading list bookworm" var keys = new[] { 3919, 626, 17201, 1300, 25782, 9800, 32016, 13571, 43582, 20189, 1891, 10424, 9631, 16497, 12984, 20020, 24046, 13159, 805, 15817, 5239, 2070, 13466, 32932, 8095, 1351, 25323 }; - var settings = new OpenAIPromptExecutionSettings(); + var settings = new OpenAIPromptExecutionSettings + { + // This will make the model try its best to avoid any of the above related words. + //-100 to potentially ban all the tokens from the list. + TokenSelectionBiases = keys.ToDictionary(key => key, key => -100) + }; - // This will make the model try its best to avoid any of the above related words. - //-100 to potentially ban all the tokens from the list. - settings.TokenSelectionBiases = keys.ToDictionary(key => key, key => -100); - - Console.WriteLine("Chat content:"); - Console.WriteLine("------------------------"); + WriteLine("Chat content:"); + WriteLine("------------------------"); var chatHistory = new ChatHistory("You are a librarian expert"); @@ -66,13 +71,17 @@ public static class Example49_LogitBias /// /// Outputs the last message of the chat history /// - private static Task MessageOutputAsync(ChatHistory chatHistory) + private Task MessageOutputAsync(ChatHistory chatHistory) { var message = chatHistory.Last(); - Console.WriteLine($"{message.Role}: {message.Content}"); - Console.WriteLine("------------------------"); + WriteLine($"{message.Role}: {message.Content}"); + WriteLine("------------------------"); return Task.CompletedTask; } + + public Example49_LogitBias(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs b/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs deleted file mode 100644 index 4b6979221d..0000000000 --- a/dotnet/samples/KernelSyntaxExamples/Example52_ApimAuth.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Azure.Core.Pipeline; -using Azure.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using RepoUtils; - -public static class Example52_ApimAuth -{ - public static async Task RunAsync() - { - // Azure API Management details - // For more information see 'Protect your Azure OpenAI API keys with Azure API Management' here: https://learn.microsoft.com/en-us/semantic-kernel/deploy/ - var apimUri = new Uri(Env.Var("Apim__Endpoint")); - var subscriptionKey = Env.Var("Apim__SubscriptionKey"); - - // Use interactive browser login - string[] scopes = new string[] { "https://cognitiveservices.azure.com/.default" }; - var credential = new InteractiveBrowserCredential(); - var requestContext = new TokenRequestContext(scopes); - var accessToken = await credential.GetTokenAsync(requestContext); - - // Create HttpClient and include subscription key as a default header - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey); - - // Configure OpenAIClient to use - // - Custom HttpClient with subscription key header - // - Diagnostics to log error response headers from APIM to aid problem determination - // - Authentication using BearerTokenCredential retrieved via interactive browser login - var clientOptions = new OpenAIClientOptions - { - Transport = new HttpClientTransport(httpClient), - Diagnostics = - { - LoggedHeaderNames = { "ErrorSource", "ErrorReason", "ErrorMessage", "ErrorScope", "ErrorSection", "ErrorStatusCode" }, - } - }; - var openAIClient = new OpenAIClient(apimUri, new BearerTokenCredential(accessToken), clientOptions); - - IKernelBuilder builder = Kernel.CreateBuilder(); - builder.Services.AddLogging(c => c.SetMinimumLevel(LogLevel.Warning).AddConsole()); - builder.AddAzureOpenAIChatCompletion( - deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, - openAIClient: openAIClient); - Kernel kernel = builder.Build(); - - // Load semantic plugin defined with prompt templates - string folder = RepoFiles.SamplePluginsPath(); - - kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "FunPlugin")); - - // Run - var result = await kernel.InvokeAsync( - kernel.Plugins["FunPlugin"]["Excuses"], - new() { ["input"] = "I have no homework" } - ); - Console.WriteLine(result.GetValue()); - - httpClient.Dispose(); - } -} - -public class BearerTokenCredential : TokenCredential -{ - private readonly AccessToken _accessToken; - - // Constructor that takes a Bearer token string and its expiration date - public BearerTokenCredential(AccessToken accessToken) - { - this._accessToken = accessToken; - } - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return this._accessToken; - } - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return new ValueTask(this._accessToken); - } -} diff --git a/dotnet/samples/KernelSyntaxExamples/Example52_CustomOpenAIClient.cs b/dotnet/samples/KernelSyntaxExamples/Example52_CustomOpenAIClient.cs new file mode 100644 index 0000000000..5512db6de8 --- /dev/null +++ b/dotnet/samples/KernelSyntaxExamples/Example52_CustomOpenAIClient.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Azure; +using Azure.AI.OpenAI; +using Azure.Core.Pipeline; +using Microsoft.SemanticKernel; +using RepoUtils; + +public static class Example52_CustomOpenAIClient +{ + public static async Task RunAsync() + { + Console.WriteLine("======== Using a custom OpenAI client ========"); + + string endpoint = TestConfiguration.AzureOpenAI.Endpoint; + string deploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; + string apiKey = TestConfiguration.AzureOpenAI.ApiKey; + + if (endpoint is null || deploymentName is null || apiKey is null) + { + Console.WriteLine("Azure OpenAI credentials not found. Skipping example."); + return; + } + + // Create an HttpClient and include your custom header(s) + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("x-my-custom-header", "My custom value"); + + // Configure OpenAIClient to use the customized HttpClient + var clientOptions = new OpenAIClientOptions + { + Transport = new HttpClientTransport(httpClient), + }; + var openAIClient = new OpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey), clientOptions); + + IKernelBuilder builder = Kernel.CreateBuilder(); + builder.AddAzureOpenAIChatCompletion(deploymentName, openAIClient); + Kernel kernel = builder.Build(); + + // Load semantic plugin defined with prompt templates + string folder = RepoFiles.SamplePluginsPath(); + + kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "FunPlugin")); + + // Run + var result = await kernel.InvokeAsync( + kernel.Plugins["FunPlugin"]["Excuses"], + new() { ["input"] = "I have no homework" } + ); + Console.WriteLine(result.GetValue()); + + httpClient.Dispose(); + } +} diff --git a/dotnet/samples/KernelSyntaxExamples/Example54_AzureChatCompletionWithData.cs b/dotnet/samples/KernelSyntaxExamples/Example54_AzureChatCompletionWithData.cs index eb615b609f..db63e3f08a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example54_AzureChatCompletionWithData.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example54_AzureChatCompletionWithData.cs @@ -5,6 +5,10 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using xRetry; +using Xunit.Abstractions; + +namespace Examples; /// /// This example demonstrates how to use Azure OpenAI Chat Completion with data. @@ -24,17 +28,12 @@ using Microsoft.SemanticKernel.Connectors.OpenAI; /// dotnet user-secrets set "AzureAISearch:ApiKey" "{Key from your Search service resource}" /// dotnet user-secrets set "AzureAISearch:IndexName" "..." /// -public static class Example54_AzureChatCompletionWithData +public class Example54_AzureChatCompletionWithData : BaseTest { - public static async Task RunAsync() + [RetryFact(typeof(HttpOperationException))] + public async Task ExampleWithChatCompletionAsync() { - await ExampleWithChatCompletionAsync(); - await ExampleWithKernelAsync(); - } - - private static async Task ExampleWithChatCompletionAsync() - { - Console.WriteLine("=== Example with Chat Completion ==="); + WriteLine("=== Example with Chat Completion ==="); var chatCompletion = new AzureOpenAIChatCompletionWithDataService(GetCompletionWithDataConfig()); var chatHistory = new ChatHistory(); @@ -52,9 +51,9 @@ public static class Example54_AzureChatCompletionWithData // Output // Ask: How did Emily and David meet? // Response: Emily and David, both passionate scientists, met during a research expedition to Antarctica. - Console.WriteLine($"Ask: {ask}"); - Console.WriteLine($"Response: {response}"); - Console.WriteLine(); + WriteLine($"Ask: {ask}"); + WriteLine($"Response: {response}"); + WriteLine(); // Chat history maintenance if (!string.IsNullOrEmpty(toolResponse)) @@ -69,20 +68,21 @@ public static class Example54_AzureChatCompletionWithData chatHistory.AddUserMessage(ask); // Chat Completion Streaming example - Console.WriteLine($"Ask: {ask}"); - Console.WriteLine("Response: "); + WriteLine($"Ask: {ask}"); + WriteLine("Response: "); await foreach (var word in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory)) { - Console.Write(word); + Write(word); } - Console.WriteLine(Environment.NewLine); + WriteLine(Environment.NewLine); } - private static async Task ExampleWithKernelAsync() + [RetryFact(typeof(HttpOperationException))] + public async Task ExampleWithKernelAsync() { - Console.WriteLine("=== Example with Kernel ==="); + WriteLine("=== Example with Kernel ==="); var ask = "How did Emily and David meet?"; @@ -100,9 +100,9 @@ public static class Example54_AzureChatCompletionWithData // Output // Ask: How did Emily and David meet? // Response: Emily and David, both passionate scientists, met during a research expedition to Antarctica. - Console.WriteLine($"Ask: {ask}"); - Console.WriteLine($"Response: {response.GetValue()}"); - Console.WriteLine(); + WriteLine($"Ask: {ask}"); + WriteLine($"Response: {response.GetValue()}"); + WriteLine(); // Second question based on uploaded content. ask = "What are Emily and David studying?"; @@ -112,9 +112,9 @@ public static class Example54_AzureChatCompletionWithData // Ask: What are Emily and David studying? // Response: They are passionate scientists who study glaciology, // a branch of geology that deals with the study of ice and its effects. - Console.WriteLine($"Ask: {ask}"); - Console.WriteLine($"Response: {response.GetValue()}"); - Console.WriteLine(); + WriteLine($"Ask: {ask}"); + WriteLine($"Response: {response.GetValue()}"); + WriteLine(); } /// @@ -132,4 +132,8 @@ public static class Example54_AzureChatCompletionWithData DataSourceIndex = TestConfiguration.AzureAISearch.IndexName }; } + + public Example54_AzureChatCompletionWithData(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example55_TextChunker.cs b/dotnet/samples/KernelSyntaxExamples/Example55_TextChunker.cs index 68c064b9f3..15541df97b 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example55_TextChunker.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example55_TextChunker.cs @@ -4,49 +4,23 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Threading.Tasks; using Microsoft.DeepDev; using Microsoft.ML.Tokenizers; using Microsoft.SemanticKernel.Text; using Resources; using SharpToken; +using Xunit; +using Xunit.Abstractions; using static Microsoft.SemanticKernel.Text.TextChunker; -public static class Example55_TextChunker +namespace Examples; + +public class Example55_TextChunker : BaseTest { - private const string Text = @"The city of Venice, located in the northeastern part of Italy, -is renowned for its unique geographical features. Built on more than 100 small islands in a lagoon in the -Adriatic Sea, it has no roads, just canals including the Grand Canal thoroughfare lined with Renaissance and -Gothic palaces. The central square, Piazza San Marco, contains St. Mark's Basilica, which is tiled with Byzantine -mosaics, and the Campanile bell tower offering views of the city's red roofs. - -The Amazon Rainforest, also known as Amazonia, is a moist broadleaf tropical rainforest in the Amazon biome that -covers most of the Amazon basin of South America. This basin encompasses 7 million square kilometers, of which -5.5 million square kilometers are covered by the rainforest. This region includes territory belonging to nine nations -and 3.4 million square kilometers of uncontacted tribes. The Amazon represents over half of the planet's remaining -rainforests and comprises the largest and most biodiverse tract of tropical rainforest in the world. - -The Great Barrier Reef is the world's largest coral reef system composed of over 2,900 individual reefs and 900 islands -stretching for over 2,300 kilometers over an area of approximately 344,400 square kilometers. The reef is located in the -Coral Sea, off the coast of Queensland, Australia. The Great Barrier Reef can be seen from outer space and is the world's -biggest single structure made by living organisms. This reef structure is composed of and built by billions of tiny organisms, -known as coral polyps."; - - public static Task RunAsync() + [Fact] + public void RunExample() { - RunExample(); - RunExampleForTokenCounterType(TokenCounterType.SharpToken); - RunExampleForTokenCounterType(TokenCounterType.MicrosoftML); - RunExampleForTokenCounterType(TokenCounterType.MicrosoftMLRoberta); - RunExampleForTokenCounterType(TokenCounterType.DeepDev); - RunExampleWithHeader(); - - return Task.CompletedTask; - } - - private static void RunExample() - { - Console.WriteLine("=== Text chunking ==="); + WriteLine("=== Text chunking ==="); var lines = TextChunker.SplitPlainTextLines(Text, 40); var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, 120); @@ -54,9 +28,14 @@ known as coral polyps."; WriteParagraphsToConsole(paragraphs); } - private static void RunExampleForTokenCounterType(TokenCounterType counterType) + [Theory] + [InlineData(TokenCounterType.SharpToken)] + [InlineData(TokenCounterType.MicrosoftML)] + [InlineData(TokenCounterType.MicrosoftMLRoberta)] + [InlineData(TokenCounterType.DeepDev)] + public void RunExampleForTokenCounterType(TokenCounterType counterType) { - Console.WriteLine($"=== Text chunking with a custom({counterType}) token counter ==="); + WriteLine($"=== Text chunking with a custom({counterType}) token counter ==="); var sw = new Stopwatch(); sw.Start(); var tokenCounter = s_tokenCounterFactory(counterType); @@ -65,13 +44,14 @@ known as coral polyps."; var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, 120, tokenCounter: tokenCounter); sw.Stop(); - Console.WriteLine($"Elapsed time: {sw.ElapsedMilliseconds} ms"); + WriteLine($"Elapsed time: {sw.ElapsedMilliseconds} ms"); WriteParagraphsToConsole(paragraphs); } - private static void RunExampleWithHeader() + [Fact] + public void RunExampleWithHeader() { - Console.WriteLine("=== Text chunking with chunk header ==="); + WriteLine("=== Text chunking with chunk header ==="); var lines = TextChunker.SplitPlainTextLines(Text, 40); var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, 150, chunkHeader: "DOCUMENT NAME: test.txt\n\n"); @@ -79,20 +59,20 @@ known as coral polyps."; WriteParagraphsToConsole(paragraphs); } - private static void WriteParagraphsToConsole(List paragraphs) + private void WriteParagraphsToConsole(List paragraphs) { for (var i = 0; i < paragraphs.Count; i++) { - Console.WriteLine(paragraphs[i]); + WriteLine(paragraphs[i]); if (i < paragraphs.Count - 1) { - Console.WriteLine("------------------------"); + WriteLine("------------------------"); } } } - private enum TokenCounterType + public enum TokenCounterType { SharpToken, MicrosoftML, @@ -175,4 +155,26 @@ known as coral polyps."; TokenCounterType.MicrosoftMLRoberta => (string input) => MicrosoftMLRobertaTokenCounter(input), _ => throw new ArgumentOutOfRangeException(nameof(counterType), counterType, null), }; + + private const string Text = @"The city of Venice, located in the northeastern part of Italy, +is renowned for its unique geographical features. Built on more than 100 small islands in a lagoon in the +Adriatic Sea, it has no roads, just canals including the Grand Canal thoroughfare lined with Renaissance and +Gothic palaces. The central square, Piazza San Marco, contains St. Mark's Basilica, which is tiled with Byzantine +mosaics, and the Campanile bell tower offering views of the city's red roofs. + +The Amazon Rainforest, also known as Amazonia, is a moist broadleaf tropical rainforest in the Amazon biome that +covers most of the Amazon basin of South America. This basin encompasses 7 million square kilometers, of which +5.5 million square kilometers are covered by the rainforest. This region includes territory belonging to nine nations +and 3.4 million square kilometers of uncontacted tribes. The Amazon represents over half of the planet's remaining +rainforests and comprises the largest and most biodiverse tract of tropical rainforest in the world. + +The Great Barrier Reef is the world's largest coral reef system composed of over 2,900 individual reefs and 900 islands +stretching for over 2,300 kilometers over an area of approximately 344,400 square kilometers. The reef is located in the +Coral Sea, off the coast of Queensland, Australia. The Great Barrier Reef can be seen from outer space and is the world's +biggest single structure made by living organisms. This reef structure is composed of and built by billions of tiny organisms, +known as coral polyps."; + + public Example55_TextChunker(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example56_TemplateMethodFunctionsWithMultipleArguments.cs b/dotnet/samples/KernelSyntaxExamples/Example56_TemplateMethodFunctionsWithMultipleArguments.cs index 9ad058a7c2..9e7eeaa4b1 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example56_TemplateMethodFunctionsWithMultipleArguments.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example56_TemplateMethodFunctionsWithMultipleArguments.cs @@ -1,22 +1,26 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Plugins.Core; +using Xunit; +using Xunit.Abstractions; -public static class Example56_TemplateMethodFunctionsWithMultipleArguments +namespace Examples; + +public class Example56_TemplateMethodFunctionsWithMultipleArguments : BaseTest { /// /// Show how to invoke a Method Function written in C# with multiple arguments /// from a Prompt Function written in natural language /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== TemplateMethodFunctionsWithMultipleArguments ========"); + WriteLine("======== TemplateMethodFunctionsWithMultipleArguments ========"); string serviceId = TestConfiguration.AzureOpenAI.ServiceId; string apiKey = TestConfiguration.AzureOpenAI.ApiKey; @@ -26,7 +30,7 @@ public static class Example56_TemplateMethodFunctionsWithMultipleArguments if (apiKey == null || deploymentName == null || modelId == null || endpoint == null) { - Console.WriteLine("AzureOpenAI modelId, endpoint, apiKey, or deploymentName not found. Skipping example."); + WriteLine("AzureOpenAI modelId, endpoint, apiKey, or deploymentName not found. Skipping example."); return; } @@ -53,19 +57,19 @@ public static class Example56_TemplateMethodFunctionsWithMultipleArguments "; // This allows to see the prompt before it's sent to OpenAI - Console.WriteLine("--- Rendered Prompt"); + WriteLine("--- Rendered Prompt"); var promptTemplateFactory = new KernelPromptTemplateFactory(); var promptTemplate = promptTemplateFactory.Create(new PromptTemplateConfig(FunctionDefinition)); var renderedPrompt = await promptTemplate.RenderAsync(kernel, arguments); - Console.WriteLine(renderedPrompt); + WriteLine(renderedPrompt); // Run the prompt / prompt function var haiku = kernel.CreateFunctionFromPrompt(FunctionDefinition, new OpenAIPromptExecutionSettings() { MaxTokens = 100 }); // Show the result - Console.WriteLine("--- Prompt Function result"); + WriteLine("--- Prompt Function result"); var result = await kernel.InvokeAsync(haiku, arguments); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); /* OUTPUT: @@ -79,4 +83,8 @@ Wizarding world he explores, Harry Potter's tale. */ } + + public Example56_TemplateMethodFunctionsWithMultipleArguments(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example57_KernelHooks.cs b/dotnet/samples/KernelSyntaxExamples/Example57_KernelHooks.cs index 7c19fe08b0..18b95204e9 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example57_KernelHooks.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example57_KernelHooks.cs @@ -1,55 +1,32 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using RepoUtils; +using Xunit; +using Xunit.Abstractions; -public static class Example57_KernelHooks +namespace Examples; + +public class Example57_KernelHooks : BaseTest { - private static string? s_openAIModelId; - private static string? s_openAIApiKey; - - public static async Task RunAsync() - { - Console.WriteLine("\n======== Using Function Execution Handlers ========\n"); - - s_openAIModelId = TestConfiguration.OpenAI.ChatModelId; - s_openAIApiKey = TestConfiguration.OpenAI.ApiKey; - - if (s_openAIModelId == null || s_openAIApiKey == null) - { - Console.WriteLine("OpenAI credentials not found. Skipping example."); - return; - } - - await GetUsageAsync(); - - await GetRenderedPromptAsync(); - - await ChangingResultAsync(); - - await BeforeInvokeCancellationAsync(); - - await AfterInvokeCancellationAsync(); - } - /// /// Demonstrate using kernel invocation-hooks to monitor usage: /// /// /// - private static async Task GetUsageAsync() + [Fact] + public async Task GetUsageAsync() { - Console.WriteLine("\n======== Get Usage Data ========\n"); + WriteLine("\n======== Get Usage Data ========\n"); // Create kernel instance Kernel kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( - modelId: s_openAIModelId!, - apiKey: s_openAIApiKey!) + modelId: _openAIModelId!, + apiKey: _openAIApiKey!) .Build(); // Initialize prompt @@ -63,18 +40,18 @@ public static class Example57_KernelHooks // Define hooks void MyPreHandler(object? sender, FunctionInvokingEventArgs e) { - Console.WriteLine($"{e.Function.Name} : Pre Execution Handler - Triggered"); + WriteLine($"{e.Function.Name} : Pre Execution Handler - Triggered"); } void MyRemovedPreExecutionHandler(object? sender, FunctionInvokingEventArgs e) { - Console.WriteLine($"{e.Function.Name} : Pre Execution Handler - Should not trigger"); + WriteLine($"{e.Function.Name} : Pre Execution Handler - Should not trigger"); e.Cancel = true; } void MyPostExecutionHandler(object? sender, FunctionInvokedEventArgs e) { - Console.WriteLine($"{e.Function.Name} : Post Execution Handler - Usage: {e.Result.Metadata?["Usage"]?.AsJson()}"); + WriteLine($"{e.Function.Name} : Post Execution Handler - Usage: {e.Result.Metadata?["Usage"]?.AsJson()}"); } kernel.FunctionInvoking += MyPreHandler; @@ -88,7 +65,7 @@ public static class Example57_KernelHooks // Invoke prompt to trigger execution hooks. const string Input = "I missed the F1 final race"; var result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = Input }); - Console.WriteLine($"Function Result: {result}"); + WriteLine($"Function Result: {result}"); } /// @@ -96,15 +73,16 @@ public static class Example57_KernelHooks /// /// /// - private static async Task GetRenderedPromptAsync() + [Fact] + public async Task GetRenderedPromptAsync() { - Console.WriteLine("\n======== Get Rendered Prompt ========\n"); + WriteLine("\n======== Get Rendered Prompt ========\n"); // Create kernel instance Kernel kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( - modelId: s_openAIModelId!, - apiKey: s_openAIApiKey!) + modelId: _openAIModelId!, + apiKey: _openAIApiKey!) .Build(); // Initialize prompt @@ -118,16 +96,16 @@ public static class Example57_KernelHooks // Define hooks void MyRenderingHandler(object? sender, PromptRenderingEventArgs e) { - Console.WriteLine($"{e.Function.Name} : Prompt Rendering Handler - Triggered"); + WriteLine($"{e.Function.Name} : Prompt Rendering Handler - Triggered"); e.Arguments["style"] = "Seinfeld"; } void MyRenderedHandler(object? sender, PromptRenderedEventArgs e) { - Console.WriteLine($"{e.Function.Name} : Prompt Rendered Handler - Triggered"); + WriteLine($"{e.Function.Name} : Prompt Rendered Handler - Triggered"); e.RenderedPrompt += " USE SHORT, CLEAR, COMPLETE SENTENCES."; - Console.WriteLine(e.RenderedPrompt); + WriteLine(e.RenderedPrompt); } kernel.PromptRendering += MyRenderingHandler; @@ -136,22 +114,23 @@ public static class Example57_KernelHooks // Invoke prompt to trigger prompt rendering hooks. const string Input = "I missed the F1 final race"; var result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = Input }); - Console.WriteLine($"Function Result: {result.GetValue()}"); + WriteLine($"Function Result: {result.GetValue()}"); } /// /// Demonstrate using kernel invocation-hooks to post process result: /// /// - private static async Task ChangingResultAsync() + [Fact] + public async Task ChangingResultAsync() { - Console.WriteLine("\n======== Changing/Filtering Function Result ========\n"); + WriteLine("\n======== Changing/Filtering Function Result ========\n"); // Create kernel instance Kernel kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( - modelId: s_openAIModelId!, - apiKey: s_openAIApiKey!) + modelId: _openAIModelId!, + apiKey: _openAIApiKey!) .Build(); // Initialize function @@ -178,7 +157,7 @@ public static class Example57_KernelHooks // Invoke prompt to trigger execution hooks. var result = await kernel.InvokeAsync(writerFunction); - Console.WriteLine($"Function Result: {result.GetValue()}"); + WriteLine($"Function Result: {result.GetValue()}"); } /// @@ -186,15 +165,16 @@ public static class Example57_KernelHooks /// /// /// - private static async Task BeforeInvokeCancellationAsync() + [Fact] + public async Task BeforeInvokeCancellationAsync() { - Console.WriteLine("\n======== Cancelling Pipeline Execution - Invoking event ========\n"); + WriteLine("\n======== Cancelling Pipeline Execution - Invoking event ========\n"); // Create kernel instance Kernel kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( - modelId: s_openAIModelId!, - apiKey: s_openAIApiKey!) + modelId: _openAIModelId!, + apiKey: _openAIApiKey!) .Build(); // Initialize prompt @@ -208,7 +188,7 @@ public static class Example57_KernelHooks // Adding new inline handler to cancel/prevent function execution kernel.FunctionInvoking += (object? sender, FunctionInvokingEventArgs e) => { - Console.WriteLine($"{e.Function.Name} : FunctionInvoking - Cancelling before execution"); + WriteLine($"{e.Function.Name} : FunctionInvoking - Cancelling before execution"); e.Cancel = true; }; @@ -220,8 +200,16 @@ public static class Example57_KernelHooks }; // Invoke prompt to trigger execution hooks. - var result = await kernel.InvokeAsync(writerFunction); - Console.WriteLine($"Function Invocation Times: {functionInvokedCount}"); + try + { + var result = await kernel.InvokeAsync(writerFunction); + } + catch (KernelFunctionCanceledException fcex) + { + WriteLine(fcex.Message); + } + + WriteLine($"Function Invocation Times: {functionInvokedCount}"); } /// @@ -229,15 +217,16 @@ public static class Example57_KernelHooks /// /// /// - private static async Task AfterInvokeCancellationAsync() + [Fact] + public async Task AfterInvokeCancellationAsync() { - Console.WriteLine("\n======== Cancelling Pipeline Execution - Invoked event ========\n"); + WriteLine("\n======== Cancelling Pipeline Execution - Invoked event ========\n"); // Create kernel instance Kernel kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( - modelId: s_openAIModelId!, - apiKey: s_openAIApiKey!) + modelId: _openAIModelId!, + apiKey: _openAIApiKey!) .Build(); // Initialize prompts @@ -261,8 +250,31 @@ public static class Example57_KernelHooks }; // Invoke prompt to trigger execution hooks. - var result = await kernel.InvokeAsync(secondFunction); - Console.WriteLine($"Function Invoked Times: {functionInvokedCount}"); - Console.WriteLine($"Function Invoking Times: {functionInvokingCount}"); + try + { + var result = await kernel.InvokeAsync(secondFunction); + } + catch (KernelFunctionCanceledException fcex) + { + WriteLine(fcex.Message); + } + + WriteLine($"Function Invoked Times: {functionInvokedCount}"); + WriteLine($"Function Invoking Times: {functionInvokingCount}"); + } + + private readonly string? _openAIModelId; + private readonly string? _openAIApiKey; + + public Example57_KernelHooks(ITestOutputHelper output) : base(output) + { + this._openAIModelId = TestConfiguration.OpenAI.ChatModelId; + this._openAIApiKey = TestConfiguration.OpenAI.ApiKey; + + if (this._openAIModelId == null || this._openAIApiKey == null) + { + WriteLine("OpenAI credentials not found. Skipping example."); + return; + } } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureExecutionSettings.cs b/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureExecutionSettings.cs index caf414f8c8..2d8cf0e30d 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureExecutionSettings.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example58_ConfigureExecutionSettings.cs @@ -5,8 +5,11 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit.Abstractions; -public static class Example58_ConfigureExecutionSettings +namespace Examples; + +public class Example58_ConfigureExecutionSettings : BaseTest { /// /// Show how to configure model execution settings @@ -94,4 +97,8 @@ Hello! As an AI language model, I can help you with a variety of tasks, includin 4. Providing explanations and */ } + + public Example58_ConfigureExecutionSettings(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 6f628085f3..3c874fe9e0 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using Azure.AI.OpenAI; @@ -12,11 +11,16 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use OpenAI's tool calling capability via the chat completions interface. -public static class Example59_OpenAIFunctionCalling +public class Example59_OpenAIFunctionCalling : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create kernel. IKernelBuilder builder = Kernel.CreateBuilder(); @@ -46,24 +50,24 @@ public static class Example59_OpenAIFunctionCalling }, "Get_Weather_For_City", "Gets the current weather for the specified city"), }); - Console.WriteLine("======== Example 1: Use automated function calling with a non-streaming prompt ========"); + WriteLine("======== Example 1: Use automated function calling with a non-streaming prompt ========"); { OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - Console.WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))); - Console.WriteLine(); + WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))); + WriteLine(); } - Console.WriteLine("======== Example 2: Use automated function calling with a streaming prompt ========"); + WriteLine("======== Example 2: Use automated function calling with a streaming prompt ========"); { OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; await foreach (var update in kernel.InvokePromptStreamingAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))) { - Console.Write(update); + Write(update); } - Console.WriteLine(); + WriteLine(); } - Console.WriteLine("======== Example 3: Use manual function calling with a non-streaming prompt ========"); + WriteLine("======== Example 3: Use manual function calling with a non-streaming prompt ========"); { var chat = kernel.GetRequiredService(); var chatHistory = new ChatHistory(); @@ -76,7 +80,7 @@ public static class Example59_OpenAIFunctionCalling if (result.Content is not null) { - Console.Write(result.Content); + Write(result.Content); } List toolCalls = result.ToolCalls.OfType().ToList(); @@ -99,9 +103,10 @@ public static class Example59_OpenAIFunctionCalling } } - Console.WriteLine(); + WriteLine(); } + /* Uncomment this to try in a console chat loop. Console.WriteLine("======== Example 4: Use automated function calling with a streaming chat ========"); { OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; @@ -130,6 +135,10 @@ public static class Example59_OpenAIFunctionCalling chatHistory.AddAssistantMessage(sb.ToString()); Console.WriteLine(); } - } + }*/ + } + + public Example59_OpenAIFunctionCalling(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example60_AdvancedMethodFunctions.cs b/dotnet/samples/KernelSyntaxExamples/Example60_AdvancedMethodFunctions.cs index ce73c9e54b..e2c58bda2a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example60_AdvancedMethodFunctions.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example60_AdvancedMethodFunctions.cs @@ -6,23 +6,23 @@ using System.Globalization; using System.Text.Json; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows different ways how to define and execute method functions using custom and primitive types. -public static class Example60_AdvancedMethodFunctions +public class Example60_AdvancedMethodFunctions : BaseTest { - public static async Task RunAsync() - { - await MethodFunctionsChainingAsync(); - } - #region Method Functions Chaining /// /// This example executes Function1, which in turn executes Function2. /// - private static async Task MethodFunctionsChainingAsync() + [Fact] + public async Task MethodFunctionsChainingAsync() { - Console.WriteLine("Running Method Function Chaining example..."); + WriteLine("Running Method Function Chaining example..."); var kernel = new Kernel(); @@ -30,8 +30,8 @@ public static class Example60_AdvancedMethodFunctions var customType = await kernel.InvokeAsync(functions["Function1"]); - Console.WriteLine($"CustomType.Number: {customType!.Number}"); // 2 - Console.WriteLine($"CustomType.Text: {customType.Text}"); // From Function1 + From Function2 + WriteLine($"CustomType.Number: {customType!.Number}"); // 2 + WriteLine($"CustomType.Text: {customType.Text}"); // From Function1 + From Function2 } /// @@ -39,7 +39,7 @@ public static class Example60_AdvancedMethodFunctions /// private sealed class FunctionsChainingPlugin { - public const string PluginName = nameof(FunctionsChainingPlugin); + private const string PluginName = nameof(FunctionsChainingPlugin); [KernelFunction] public async Task Function1Async(Kernel kernel) @@ -115,4 +115,8 @@ public static class Example60_AdvancedMethodFunctions } #endregion + + public Example60_AdvancedMethodFunctions(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs b/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs index ae71e5604d..29a434c908 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example61_MultipleLLMs.cs @@ -1,60 +1,42 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using xRetry; +using Xunit.Abstractions; -public static class Example61_MultipleLLMs +namespace Examples; + +public class Example61_MultipleLLMs : BaseTest { /// /// Show how to run a prompt function and specify a specific service to use. /// - public static async Task RunAsync() + [RetryFact(typeof(HttpOperationException))] + public async Task RunAsync() { - Console.WriteLine("======== Example61_MultipleLLMs ========"); - - string azureApiKey = TestConfiguration.AzureOpenAI.ApiKey; - string azureDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string azureModelId = TestConfiguration.AzureOpenAI.ChatModelId; - string azureEndpoint = TestConfiguration.AzureOpenAI.Endpoint; - - if (azureApiKey == null || azureDeploymentName == null || azureModelId == null || azureEndpoint == null) - { - Console.WriteLine("AzureOpenAI endpoint, apiKey, deploymentName or modelId 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; - } - Kernel kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( - deploymentName: azureDeploymentName, - endpoint: azureEndpoint, - apiKey: azureApiKey, + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, serviceId: "AzureOpenAIChat", - modelId: azureModelId) + modelId: TestConfiguration.AzureOpenAI.ChatModelId) .AddOpenAIChatCompletion( - modelId: openAIModelId, - apiKey: openAIApiKey, + modelId: TestConfiguration.OpenAI.ChatModelId, + apiKey: TestConfiguration.OpenAI.ApiKey, serviceId: "OpenAIChat") .Build(); await RunByServiceIdAsync(kernel, "AzureOpenAIChat"); - await RunByModelIdAsync(kernel, openAIModelId); - await RunByFirstModelIdAsync(kernel, "gpt-4-1106-preview", azureModelId, openAIModelId); + await RunByModelIdAsync(kernel, TestConfiguration.OpenAI.ChatModelId); + await RunByFirstModelIdAsync(kernel, "gpt-4-1106-preview", TestConfiguration.AzureOpenAI.ChatModelId, TestConfiguration.OpenAI.ChatModelId); } - public static async Task RunByServiceIdAsync(Kernel kernel, string serviceId) + private async Task RunByServiceIdAsync(Kernel kernel, string serviceId) { - Console.WriteLine($"======== Service Id: {serviceId} ========"); + WriteLine($"======== Service Id: {serviceId} ========"); var prompt = "Hello AI, what can you do for me?"; @@ -64,12 +46,12 @@ public static class Example61_MultipleLLMs { serviceId, new PromptExecutionSettings() } }; var result = await kernel.InvokePromptAsync(prompt, arguments); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); } - public static async Task RunByModelIdAsync(Kernel kernel, string modelId) + private async Task RunByModelIdAsync(Kernel kernel, string modelId) { - Console.WriteLine($"======== Model Id: {modelId} ========"); + WriteLine($"======== Model Id: {modelId} ========"); var prompt = "Hello AI, what can you do for me?"; @@ -79,12 +61,12 @@ public static class Example61_MultipleLLMs { ModelId = modelId })); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); } - public static async Task RunByFirstModelIdAsync(Kernel kernel, params string[] modelIds) + private async Task RunByFirstModelIdAsync(Kernel kernel, params string[] modelIds) { - Console.WriteLine($"======== Model Ids: {string.Join(", ", modelIds)} ========"); + WriteLine($"======== Model Ids: {string.Join(", ", modelIds)} ========"); var prompt = "Hello AI, what can you do for me?"; @@ -98,6 +80,10 @@ public static class Example61_MultipleLLMs var function = kernel.CreateFunctionFromPrompt(promptConfig); var result = await kernel.InvokeAsync(function); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); + } + + public Example61_MultipleLLMs(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs b/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs index f886afd14f..adb85f5112 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example62_CustomAIServiceSelector.cs @@ -7,55 +7,40 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Services; +using Xunit; +using Xunit.Abstractions; -public static class Example62_CustomAIServiceSelector +namespace Examples; + +public class Example62_CustomAIServiceSelector : BaseTest { /// /// Show how to use a custom AI service selector to select a specific model /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Example62_CustomAIServiceSelector ========"); - - string azureApiKey = TestConfiguration.AzureOpenAI.ApiKey; - string azureDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string azureModelId = TestConfiguration.AzureOpenAI.ChatModelId; - string azureEndpoint = TestConfiguration.AzureOpenAI.Endpoint; - - if (azureApiKey == null || azureDeploymentName == null || azureModelId == null || azureEndpoint == null) - { - Console.WriteLine("AzureOpenAI endpoint, apiKey, deploymentName or modelId 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; - } + WriteLine("======== Example62_CustomAIServiceSelector ========"); // Build a kernel with multiple chat completion services var builder = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( - deploymentName: azureDeploymentName, - endpoint: azureEndpoint, - apiKey: azureApiKey, + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, + apiKey: TestConfiguration.AzureOpenAI.ApiKey, serviceId: "AzureOpenAIChat", - modelId: azureModelId) + modelId: TestConfiguration.AzureOpenAI.ChatModelId) .AddOpenAIChatCompletion( - modelId: openAIModelId, - apiKey: openAIApiKey, + modelId: TestConfiguration.OpenAI.ChatModelId, + apiKey: TestConfiguration.OpenAI.ApiKey, serviceId: "OpenAIChat"); - builder.Services.AddSingleton(new GptAIServiceSelector()); // Use the custom AI service selector to select the GPT model + builder.Services.AddSingleton(new GptAIServiceSelector(this.Output)); // Use the custom AI service selector to select the GPT model Kernel kernel = builder.Build(); // This invocation is done with the model selected by the custom selector var prompt = "Hello AI, what can you do for me?"; var result = await kernel.InvokePromptAsync(prompt); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); } /// @@ -66,6 +51,13 @@ public static class Example62_CustomAIServiceSelector /// private sealed class GptAIServiceSelector : IAIServiceSelector { + private readonly ITestOutputHelper _output; + + public GptAIServiceSelector(ITestOutputHelper output) + { + this._output = output; + } + public bool TrySelectAIService( Kernel kernel, KernelFunction function, KernelArguments arguments, [NotNullWhen(true)] out T? service, out PromptExecutionSettings? serviceSettings) where T : class, IAIService @@ -77,7 +69,7 @@ public static class Example62_CustomAIServiceSelector var endpoint = serviceToCheck.GetEndpoint(); if (!string.IsNullOrEmpty(serviceModelId) && serviceModelId.StartsWith("gpt", StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine($"Selected model: {serviceModelId} {endpoint}"); + this._output.WriteLine($"Selected model: {serviceModelId} {endpoint}"); service = serviceToCheck; serviceSettings = new OpenAIPromptExecutionSettings(); return true; @@ -89,4 +81,8 @@ public static class Example62_CustomAIServiceSelector return false; } } + + public Example62_CustomAIServiceSelector(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example63_ChatCompletionPrompts.cs b/dotnet/samples/KernelSyntaxExamples/Example63_ChatCompletionPrompts.cs index e6325e082a..5b8b45d50a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example63_ChatCompletionPrompts.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example63_ChatCompletionPrompts.cs @@ -1,13 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use chat completion standardized prompts. -public static class Example63_ChatCompletionPrompts +public class Example63_ChatCompletionPrompts : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { const string ChatPrompt = @" What is Seattle? @@ -23,17 +27,21 @@ public static class Example63_ChatCompletionPrompts var chatSemanticFunction = kernel.CreateFunctionFromPrompt(ChatPrompt); var chatPromptResult = await kernel.InvokeAsync(chatSemanticFunction); - Console.WriteLine("Chat Prompt:"); - Console.WriteLine(ChatPrompt); - Console.WriteLine("Chat Prompt Result:"); - Console.WriteLine(chatPromptResult); + WriteLine("Chat Prompt:"); + WriteLine(ChatPrompt); + WriteLine("Chat Prompt Result:"); + WriteLine(chatPromptResult); - Console.WriteLine("Chat Prompt Streaming Result:"); + WriteLine("Chat Prompt Streaming Result:"); + string completeMessage = string.Empty; await foreach (var message in kernel.InvokeStreamingAsync(chatSemanticFunction)) { - Console.Write(message); + completeMessage += message; + Write(message); } - Console.WriteLine(); + + WriteLine("---------- Streamed Content ----------"); + WriteLine(completeMessage); /* Chat Prompt: @@ -51,4 +59,8 @@ public static class Example63_ChatCompletionPrompts } */ } + + public Example63_ChatCompletionPrompts(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example64_MultiplePromptTemplates.cs b/dotnet/samples/KernelSyntaxExamples/Example64_MultiplePromptTemplates.cs index cb8a439e02..2e792e0ed0 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example64_MultiplePromptTemplates.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example64_MultiplePromptTemplates.cs @@ -1,54 +1,46 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.PromptTemplates.Handlebars; +using xRetry; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use multiple prompt template formats. -public static class Example64_MultiplePromptTemplates +public class Example64_MultiplePromptTemplates : BaseTest { /// /// Show how to combine multiple prompt template factories. /// - public static async Task RunAsync() + [RetryTheory(typeof(HttpOperationException))] + [InlineData("semantic-kernel", "Hello AI, my name is {{$name}}. What is the origin of my name?")] + [InlineData("handlebars", "Hello AI, my name is {{name}}. What is the origin of my name?")] + public Task RunAsync(string templateFormat, string prompt) { - Console.WriteLine("======== Example64_MultiplePromptTemplates ========"); - - string apiKey = TestConfiguration.AzureOpenAI.ApiKey; - string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string chatModelId = TestConfiguration.AzureOpenAI.ChatModelId; - string endpoint = TestConfiguration.AzureOpenAI.Endpoint; - - if (apiKey == null || chatDeploymentName == null || endpoint == null || chatModelId == null) - { - Console.WriteLine("Azure endpoint, apiKey, deploymentName or modelId not found. Skipping example."); - return; - } + WriteLine("======== Example64_MultiplePromptTemplates ========"); Kernel kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( - deploymentName: chatDeploymentName, - endpoint: endpoint, + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, + endpoint: TestConfiguration.AzureOpenAI.Endpoint, serviceId: "AzureOpenAIChat", - apiKey: apiKey, - modelId: chatModelId) + apiKey: TestConfiguration.AzureOpenAI.ApiKey, + modelId: TestConfiguration.AzureOpenAI.ChatModelId) .Build(); var promptTemplateFactory = new AggregatorPromptTemplateFactory( new KernelPromptTemplateFactory(), new HandlebarsPromptTemplateFactory()); - var skPrompt = "Hello AI, my name is {{$name}}. What is the origin of my name?"; - var handlebarsPrompt = "Hello AI, my name is {{name}}. What is the origin of my name?"; - - await RunPromptAsync(kernel, skPrompt, "semantic-kernel", promptTemplateFactory); - await RunPromptAsync(kernel, handlebarsPrompt, "handlebars", promptTemplateFactory); + return RunPromptAsync(kernel, prompt, templateFormat, promptTemplateFactory); } - public static async Task RunPromptAsync(Kernel kernel, string prompt, string templateFormat, IPromptTemplateFactory promptTemplateFactory) + private async Task RunPromptAsync(Kernel kernel, string prompt, string templateFormat, IPromptTemplateFactory promptTemplateFactory) { - Console.WriteLine($"======== {templateFormat} : {prompt} ========"); + WriteLine($"======== {templateFormat} : {prompt} ========"); var function = kernel.CreateFunctionFromPrompt( promptConfig: new PromptTemplateConfig() @@ -66,6 +58,10 @@ public static class Example64_MultiplePromptTemplates }; var result = await kernel.InvokeAsync(function, arguments); - Console.WriteLine(result.GetValue()); + WriteLine(result.GetValue()); + } + + public Example64_MultiplePromptTemplates(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs index 99806a8195..40d81a213b 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example65_HandlebarsPlanner.cs @@ -8,40 +8,24 @@ using Microsoft.SemanticKernel.Planning.Handlebars; using Microsoft.SemanticKernel.Plugins.OpenApi; using Plugins.DictionaryPlugin; using RepoUtils; +using xRetry; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use the Handlebars sequential planner. -public static class Example65_HandlebarsPlanner +public class Example65_HandlebarsPlanner : BaseTest { private static int s_sampleIndex; private const string CourseraPluginName = "CourseraPlugin"; - /// - /// Show how to create a plan with Handlebars and execute it. - /// - public static async Task RunAsync() + private void WriteSampleHeading(string name) { - s_sampleIndex = 1; - - // Plugin with primitive types as inputs and outputs - await RunDictionaryWithBasicTypesSampleAsync(); - await RunPoetrySampleAsync(); - await RunBookSampleAsync(); - await PlanNotPossibleSampleAsync(); - - // Plugin with Complex Types as inputs and outputs - await RunLocalDictionaryWithComplexTypesSampleAsync(); - - // OpenAPI plugin - await RunCourseraSampleAsync(shouldPrintPrompt: true); + WriteLine($"======== [Handlebars Planner] Sample {s_sampleIndex++} - Create and Execute Plan with: {name} ========"); } - - private static void WriteSampleHeadingToConsole(string name) - { - Console.WriteLine($"======== [Handlebars Planner] Sample {s_sampleIndex++} - Create and Execute Plan with: {name} ========"); - } - - private static async Task RunSampleAsync(string goal, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames) + private async Task RunSampleAsync(string goal, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames) { string apiKey = TestConfiguration.AzureOpenAI.ApiKey; string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; @@ -50,7 +34,7 @@ public static class Example65_HandlebarsPlanner if (apiKey == null || chatDeploymentName == null || chatModelId == null || endpoint == null) { - Console.WriteLine("Azure endpoint, apiKey, deploymentName, or modelId not found. Skipping example."); + WriteLine("Azure endpoint, apiKey, deploymentName, or modelId not found. Skipping example."); return; } @@ -101,7 +85,7 @@ public static class Example65_HandlebarsPlanner AllowLoops = allowLoopsInPlan }); - Console.WriteLine($"Goal: {goal}"); + WriteLine($"Goal: {goal}"); // Create the plan var plan = await planner.CreatePlanAsync(kernel, goal); @@ -109,19 +93,21 @@ public static class Example65_HandlebarsPlanner // Print the prompt template if (shouldPrintPrompt && plan.Prompt is not null) { - Console.WriteLine($"\nPrompt template:\n{plan.Prompt}"); + WriteLine($"\nPrompt template:\n{plan.Prompt}"); } - Console.WriteLine($"\nOriginal plan:\n{plan}"); + WriteLine($"\nOriginal plan:\n{plan}"); // Execute the plan var result = await plan.InvokeAsync(kernel); - Console.WriteLine($"\nResult:\n{result}\n"); + WriteLine($"\nResult:\n{result}\n"); } - private static async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false) + [RetryTheory(typeof(HttpOperationException))] + [InlineData(false)] + public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false) { - WriteSampleHeadingToConsole("Plan Not Possible"); + WriteSampleHeading("Plan Not Possible"); try { @@ -142,20 +128,25 @@ public static class Example65_HandlebarsPlanner Therefore, I cannot create a Handlebars template to achieve the specified goal with the available helpers. Additional helpers may be required. */ - Console.WriteLine($"\n{ex.Message}\n"); + WriteLine($"\n{ex.Message}\n"); } } - private static async Task RunCourseraSampleAsync(bool shouldPrintPrompt = false) + [RetryTheory(typeof(HttpOperationException))] + [InlineData(true)] + + public Task RunCourseraSampleAsync(bool shouldPrintPrompt = false) { - WriteSampleHeadingToConsole("Coursera OpenAPI Plugin"); - await RunSampleAsync("Show me courses about Artificial Intelligence.", shouldPrintPrompt, CourseraPluginName); + this.WriteSampleHeading("Coursera OpenAPI Plugin"); + return RunSampleAsync("Show me courses about Artificial Intelligence.", shouldPrintPrompt, CourseraPluginName); } - private static async Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = false) + [RetryTheory(typeof(HttpOperationException))] + [InlineData(false)] + public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = false) { - WriteSampleHeadingToConsole("Basic Types using Local Dictionary Plugin"); - await RunSampleAsync("Get a random word and its definition.", shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName); + this.WriteSampleHeading("Basic Types using Local Dictionary Plugin"); + return RunSampleAsync("Get a random word and its definition.", shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName); /* Original plan: {{!-- Step 1: Get a random word --}} @@ -172,10 +163,12 @@ public static class Example65_HandlebarsPlanner */ } - private static async Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt = false) + [RetryTheory(typeof(HttpOperationException))] + [InlineData(true)] + public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt = false) { - WriteSampleHeadingToConsole("Complex Types using Local Dictionary Plugin"); - await RunSampleAsync("Teach me two random words and their definition.", shouldPrintPrompt, ComplexParamsDictionaryPlugin.PluginName); + this.WriteSampleHeading("Complex Types using Local Dictionary Plugin"); + return RunSampleAsync("Teach me two random words and their definition.", shouldPrintPrompt, ComplexParamsDictionaryPlugin.PluginName); /* Original Plan: {{!-- Step 1: Get two random dictionary entries --}} @@ -206,10 +199,12 @@ public static class Example65_HandlebarsPlanner */ } - private static async Task RunPoetrySampleAsync(bool shouldPrintPrompt = false) + [RetryTheory(typeof(HttpOperationException))] + [InlineData(false)] + public Task RunPoetrySampleAsync(bool shouldPrintPrompt = false) { - WriteSampleHeadingToConsole("Multiple Plugins"); - await RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", shouldPrintPrompt, "SummarizePlugin", "WriterPlugin"); + this.WriteSampleHeading("Multiple Plugins"); + return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", shouldPrintPrompt, "SummarizePlugin", "WriterPlugin"); /* Original plan: {{!-- Step 1: Initialize the scenario for the poem --}} @@ -233,10 +228,12 @@ public static class Example65_HandlebarsPlanner */ } - private static async Task RunBookSampleAsync(bool shouldPrintPrompt = false) + [RetryTheory(typeof(HttpOperationException))] + [InlineData(false)] + public Task RunBookSampleAsync(bool shouldPrintPrompt = false) { - WriteSampleHeadingToConsole("Loops and Conditionals"); - await RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", shouldPrintPrompt, "WriterPlugin", "MiscPlugin"); + this.WriteSampleHeading("Loops and Conditionals"); + return RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", shouldPrintPrompt, "WriterPlugin", "MiscPlugin"); /* Original plan: {{!-- Step 1: Initialize the book title and chapter count --}} @@ -260,4 +257,8 @@ public static class Example65_HandlebarsPlanner {{/each}} */ } + + public Example65_HandlebarsPlanner(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs index c2f4f48fd1..e42883b52f 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs @@ -1,18 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Planning; using Microsoft.SemanticKernel.Plugins.Core; using Plugins; +using Xunit; +using Xunit.Abstractions; -public static class Example66_FunctionCallingStepwisePlanner +namespace Examples; + +public class Example66_FunctionCallingStepwisePlanner : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - string[] questions = new string[] - { + string[] questions = { "What is the current hour number, plus 5?", "What is 387 minus 22? Email the solution to John and Mary.", "Write a limerick, translate it to Spanish, and send it to Jane", @@ -30,7 +33,7 @@ public static class Example66_FunctionCallingStepwisePlanner foreach (var question in questions) { FunctionCallingStepwisePlannerResult result = await planner.ExecuteAsync(kernel, question); - Console.WriteLine($"Q: {question}\nA: {result.FinalAnswer}"); + WriteLine($"Q: {question}\nA: {result.FinalAnswer}"); // You can uncomment the line below to see the planner's process for completing the request. // Console.WriteLine($"Chat history:\n{System.Text.Json.JsonSerializer.Serialize(result.ChatHistory)}"); @@ -44,11 +47,9 @@ public static class Example66_FunctionCallingStepwisePlanner private static Kernel InitializeKernel() { Kernel kernel = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion( - deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, - endpoint: TestConfiguration.AzureOpenAI.Endpoint, - apiKey: TestConfiguration.AzureOpenAI.ApiKey, - modelId: TestConfiguration.AzureOpenAI.ChatModelId) + .AddOpenAIChatCompletion( + apiKey: TestConfiguration.OpenAI.ApiKey, + modelId: "gpt-3.5-turbo-1106") .Build(); kernel.ImportPluginFromType(); @@ -57,4 +58,8 @@ public static class Example66_FunctionCallingStepwisePlanner return kernel; } + + public Example66_FunctionCallingStepwisePlanner(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example67_KernelStreaming.cs b/dotnet/samples/KernelSyntaxExamples/Example67_KernelStreaming.cs index 9cc4c95fc2..b7d71da514 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example67_KernelStreaming.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example67_KernelStreaming.cs @@ -1,17 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use multiple prompt template formats. -public static class Example67_KernelStreaming +public class Example67_KernelStreaming : BaseTest { /// /// Show how to combine multiple prompt template factories. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { string apiKey = TestConfiguration.AzureOpenAI.ApiKey; string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; @@ -20,7 +24,7 @@ public static class Example67_KernelStreaming if (apiKey == null || chatDeploymentName == null || chatModelId == null || endpoint == null) { - Console.WriteLine("Azure endpoint, apiKey, deploymentName or modelId not found. Skipping example."); + WriteLine("Azure endpoint, apiKey, deploymentName or modelId not found. Skipping example."); return; } @@ -37,22 +41,32 @@ public static class Example67_KernelStreaming var roleDisplayed = false; - Console.WriteLine("\n=== Prompt Function - Streaming ===\n"); + WriteLine("\n=== Prompt Function - Streaming ===\n"); + string fullContent = string.Empty; // Streaming can be of any type depending on the underlying service the function is using. await foreach (var update in kernel.InvokeStreamingAsync(funnyParagraphFunction)) { // You will be always able to know the type of the update by checking the Type property. if (!roleDisplayed && update.Role.HasValue) { - Console.WriteLine($"Role: {update.Role}"); + WriteLine($"Role: {update.Role}"); + fullContent += $"Role: {update.Role}\n"; roleDisplayed = true; } if (update.Content is { Length: > 0 }) { - Console.Write(update.Content); + fullContent += update.Content; + Write(update.Content); } } + + WriteLine("\n------ Streamed Content ------\n"); + WriteLine(fullContent); + } + + public Example67_KernelStreaming(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example68_GPTVision.cs b/dotnet/samples/KernelSyntaxExamples/Example68_GPTVision.cs index 3ff82df1f6..8011f79b57 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example68_GPTVision.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example68_GPTVision.cs @@ -4,11 +4,16 @@ using System; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to use GPT Vision model with different content types (text and image). -public static class Example68_GPTVision +public class Example68_GPTVision : BaseTest { - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { const string ImageUri = "https://upload.wikimedia.org/wikipedia/commons/d/d5/Half-timbered_mansion%2C_Zirkel%2C_East_view.jpg"; @@ -28,8 +33,10 @@ public static class Example68_GPTVision var reply = await chatCompletionService.GetChatMessageContentAsync(chatHistory); - Console.WriteLine(reply.Content); + WriteLine(reply.Content); + } - Console.ReadKey(); + public Example68_GPTVision(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example69_MutableKernelPlugin.cs b/dotnet/samples/KernelSyntaxExamples/Example69_MutableKernelPlugin.cs index cc0e56513e..eb0889a7dd 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example69_MutableKernelPlugin.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example69_MutableKernelPlugin.cs @@ -5,14 +5,19 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; // This example shows how to create a mutable . -public static class Example69_MutableKernelPlugin +public class Example69_MutableKernelPlugin : BaseTest { /// /// Show how to create a mutable . /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { var plugin = new MutableKernelPlugin("Plugin"); plugin.AddFunction(KernelFunctionFactory.CreateFromMethod(() => "Plugin.Function", "Function")); @@ -22,7 +27,7 @@ public static class Example69_MutableKernelPlugin var result = await kernel.InvokeAsync(kernel.Plugins["Plugin"]["Function"]); - Console.WriteLine($"Result: {result}"); + WriteLine($"Result: {result}"); } /// @@ -73,4 +78,8 @@ public static class Example69_MutableKernelPlugin /// public override IEnumerator GetEnumerator() => this._functions.Values.GetEnumerator(); } + + public Example69_MutableKernelPlugin(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example70_Agents.cs b/dotnet/samples/KernelSyntaxExamples/Example70_Agents.cs index 8a17164744..c18a5cac98 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example70_Agents.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example70_Agents.cs @@ -7,12 +7,16 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Experimental.Agents; using Plugins; using Resources; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// Showcase Open AI Agent integration with semantic kernel: /// https://platform.openai.com/docs/api-reference/agents /// -public static class Example70_Agent +public class Example70_Agent : BaseTest { /// /// Specific model is required that supports agents and function calling. @@ -20,42 +24,17 @@ public static class Example70_Agent /// private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106"; - /// - /// Show how to define an use a single agent using multiple patterns. - /// - public static async Task RunAsync() - { - Console.WriteLine("======== Example70_Agent ========"); - - if (TestConfiguration.OpenAI.ApiKey == null) - { - Console.WriteLine("OpenAI apiKey not found. Skipping example."); - return; - } - - // "Hello agent" - await RunSimpleChatAsync(); - - // Run agent with "method" tool/function - await RunWithMethodFunctionsAsync(); - - // Run agent with "prompt" tool/function - await RunWithPromptFunctionsAsync(); - - // Run agent as function - await RunAsFunctionAsync(); - } - /// /// Chat using the "Parrot" agent. /// Tools/functions: None /// - private static async Task RunSimpleChatAsync() + [Fact] + public Task RunSimpleChatAsync() { - Console.WriteLine("======== Run:SimpleChat ========"); + WriteLine("======== Run:SimpleChat ========"); // Call the common chat-loop - await ChatAsync( + return ChatAsync( "Agents.ParrotAgent.yaml", // Defined under ./Resources/Agents plugin: null, // No plugin arguments: new KernelArguments { { "count", 3 } }, @@ -68,14 +47,15 @@ public static class Example70_Agent /// Chat using the "Tool" agent and a method function. /// Tools/functions: MenuPlugin /// - private static async Task RunWithMethodFunctionsAsync() + [Fact] + public Task RunWithMethodFunctionsAsync() { - Console.WriteLine("======== Run:WithMethodFunctions ========"); + WriteLine("======== Run:WithMethodFunctions ========"); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); // Call the common chat-loop - await ChatAsync( + return ChatAsync( "Agents.ToolAgent.yaml", // Defined under ./Resources/Agents plugin, arguments: null, @@ -89,9 +69,10 @@ public static class Example70_Agent /// Chat using the "Tool" agent and a prompt function. /// Tools/functions: spellChecker prompt function /// - private static async Task RunWithPromptFunctionsAsync() + [Fact] + public Task RunWithPromptFunctionsAsync() { - Console.WriteLine("======== WithPromptFunctions ========"); + WriteLine("======== WithPromptFunctions ========"); // Create a prompt function. var function = KernelFunctionFactory.CreateFromPrompt( @@ -102,7 +83,7 @@ public static class Example70_Agent var plugin = KernelPluginFactory.CreateFromFunctions("spelling", "Spelling functions", new[] { function }); // Call the common chat-loop - await ChatAsync( + return ChatAsync( "Agents.ToolAgent.yaml", // Defined under ./Resources/Agents plugin, arguments: null, @@ -115,9 +96,10 @@ public static class Example70_Agent /// /// Invoke agent just like any other . /// - private static async Task RunAsFunctionAsync() + [Fact] + public async Task RunAsFunctionAsync() { - Console.WriteLine("======== Run:AsFunction ========"); + WriteLine("======== Run:AsFunction ========"); // Create parrot agent, same as the other cases. var agent = @@ -132,7 +114,7 @@ public static class Example70_Agent var response = await agent.AsPlugin().InvokeAsync("Practice makes perfect.", new KernelArguments { { "count", 2 } }); // Display result. - Console.WriteLine(response ?? $"No response from agent: {agent.Id}"); + WriteLine(response ?? $"No response from agent: {agent.Id}"); } finally { @@ -149,7 +131,7 @@ public static class Example70_Agent /// 4. Create a chat-thread /// 5. Process the provided "messages" on the chat-thread /// - private static async Task ChatAsync( + private async Task ChatAsync( string resourcePath, KernelPlugin? plugin = null, KernelArguments? arguments = null, @@ -191,4 +173,8 @@ public static class Example70_Agent agent.DeleteAsync()); } } + + public Example70_Agent(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example71_AgentDelegation.cs b/dotnet/samples/KernelSyntaxExamples/Example71_AgentDelegation.cs index ed3021f91f..a95d3d7af7 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example71_AgentDelegation.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example71_AgentDelegation.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -8,11 +7,15 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Experimental.Agents; using Plugins; using Resources; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; /// /// Showcase complex Open AI Agent interactions using semantic kernel. /// -public static class Example71_AgentDelegation +public class Example71_AgentDelegation : BaseTest { /// /// Specific model is required that supports agents and function calling. @@ -26,13 +29,14 @@ public static class Example71_AgentDelegation /// /// Show how to combine coordinate multiple agents. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { - Console.WriteLine("======== Example71_AgentDelegation ========"); + WriteLine("======== Example71_AgentDelegation ========"); if (TestConfiguration.OpenAI.ApiKey == null) { - Console.WriteLine("OpenAI apiKey not found. Skipping example."); + WriteLine("OpenAI apiKey not found. Skipping example."); return; } @@ -78,8 +82,8 @@ public static class Example71_AgentDelegation { await foreach (var message in response) { - Console.WriteLine($"[{message.Id}]"); - Console.WriteLine($"# {message.Role}: {message.Content}"); + WriteLine($"[{message.Id}]"); + WriteLine($"# {message.Role}: {message.Content}"); } } } @@ -98,4 +102,8 @@ public static class Example71_AgentDelegation return agent; } + + public Example71_AgentDelegation(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example72_AgentCollaboration.cs b/dotnet/samples/KernelSyntaxExamples/Example72_AgentCollaboration.cs index 7f9c0ea70d..fbd795362a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example72_AgentCollaboration.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example72_AgentCollaboration.cs @@ -5,12 +5,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.Experimental.Agents; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; -// ReSharper disable once InconsistentNaming /// /// Showcase complex Open AI Agent collaboration using semantic kernel. /// -public static class Example72_AgentCollaboration +public class Example72_AgentCollaboration : BaseTest { /// /// Specific model is required that supports agents and function calling. @@ -21,36 +24,14 @@ public static class Example72_AgentCollaboration // Track agents for clean-up private static readonly List s_agents = new(); - /// - /// Show how to combine and coordinate multiple agents. - /// - public static async Task RunAsync() - { - Console.WriteLine("======== Example72_AgentCollaboration ========"); - - if (TestConfiguration.OpenAI.ApiKey == null) - { - Console.WriteLine("OpenAI apiKey not found. Skipping example."); - return; - } - - // NOTE: Either of these examples produce a conversation - // whose duration may vary depending on the collaboration dynamics. - // It is sometimes possible that agreement is never achieved. - - // Explicit collaboration - await RunCollaborationAsync(); - - // Coordinate collaboration as plugin agents (equivalent to previous case - shared thread) - await RunAsPluginsAsync(); - } - /// /// Show how two agents are able to collaborate as agents on a single thread. /// - private static async Task RunCollaborationAsync() + [Fact(Skip = "This test take more than 5 minutes to execute")] + public async Task RunCollaborationAsync() { - Console.WriteLine("======== Run:Collaboration ========"); + WriteLine("======== Example72_AgentCollaboration ========"); + WriteLine("======== Run:Collaboration ========"); IAgentThread? thread = null; try { @@ -99,9 +80,11 @@ public static class Example72_AgentCollaboration /// While this may achieve an equivalent result to , /// it is not using shared thread state for agent interaction. /// - private static async Task RunAsPluginsAsync() + [Fact(Skip = "This test take more than 2 minutes to execute")] + public async Task RunAsPluginsAsync() { - Console.WriteLine("======== Run:AsPlugins ========"); + WriteLine("======== Example72_AgentCollaboration ========"); + WriteLine("======== Run:AsPlugins ========"); try { // Create copy-writer agent to generate ideas @@ -123,7 +106,7 @@ public static class Example72_AgentCollaboration var response = await coordinator.AsPlugin().InvokeAsync("concept: maps made out of egg cartons."); // Display final result - Console.WriteLine(response); + WriteLine(response); } finally { @@ -132,7 +115,7 @@ public static class Example72_AgentCollaboration } } - private async static Task CreateCopyWriterAsync(IAgent? agent = null) + private static async Task CreateCopyWriterAsync(IAgent? agent = null) { return Track( @@ -157,7 +140,7 @@ public static class Example72_AgentCollaboration .BuildAsync()); } - private static void DisplayMessages(IEnumerable messages, IAgent? agent = null) + private void DisplayMessages(IEnumerable messages, IAgent? agent = null) { foreach (var message in messages) { @@ -165,16 +148,16 @@ public static class Example72_AgentCollaboration } } - private static void DisplayMessage(IChatMessage message, IAgent? agent = null) + private void DisplayMessage(IChatMessage message, IAgent? agent = null) { - Console.WriteLine($"[{message.Id}]"); + WriteLine($"[{message.Id}]"); if (agent != null) { - Console.WriteLine($"# {message.Role}: ({agent.Name}) {message.Content}"); + WriteLine($"# {message.Role}: ({agent.Name}) {message.Content}"); } else { - Console.WriteLine($"# {message.Role}: {message.Content}"); + WriteLine($"# {message.Role}: {message.Content}"); } } @@ -184,4 +167,8 @@ public static class Example72_AgentCollaboration return agent; } + + public Example72_AgentCollaboration(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example73_AgentAuthoring.cs b/dotnet/samples/KernelSyntaxExamples/Example73_AgentAuthoring.cs index 88ce07abd0..004a3ef373 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example73_AgentAuthoring.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example73_AgentAuthoring.cs @@ -1,16 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.Experimental.Agents; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; -// ReSharper disable once InconsistentNaming /// /// Showcase hiearchical Open AI Agent interactions using semantic kernel. /// -public static class Example73_AgentAuthoring +public class Example73_AgentAuthoring : BaseTest { /// /// Specific model is required that supports agents and parallel function calling. @@ -21,28 +23,10 @@ public static class Example73_AgentAuthoring // Track agents for clean-up private static readonly List s_agents = new(); - /// - /// Show how to combine coordinate multiple agents. - /// - public static async Task RunAsync() - { - Console.WriteLine("======== Example73_AgentAuthoring ========"); - - if (TestConfiguration.OpenAI.ApiKey == null) - { - Console.WriteLine("OpenAI apiKey not found. Skipping example."); - return; - } - - // Run demo by invoking agent directly - await RunAgentAsync(); - - // Run demo by invoking agent as a plugin - await RunAsPluginAsync(); - } - - private static async Task RunAgentAsync() + [Fact(Skip = "This test take more than 2 minutes to execute")] + public async Task RunAgentAsync() { + WriteLine("======== Example73_AgentAuthoring ========"); try { // Initialize the agent with tools @@ -51,8 +35,8 @@ public static class Example73_AgentAuthoring // "Stream" messages as they become available await foreach (IChatMessage message in articleGenerator.InvokeAsync("Thai food is the best in the world")) { - Console.WriteLine($"[{message.Id}]"); - Console.WriteLine($"# {message.Role}: {message.Content}"); + WriteLine($"[{message.Id}]"); + WriteLine($"# {message.Role}: {message.Content}"); } } finally @@ -61,8 +45,10 @@ public static class Example73_AgentAuthoring } } - private static async Task RunAsPluginAsync() + [Fact(Skip = "This test take more than 2 minutes to execute")] + public async Task RunAsPluginAsync() { + WriteLine("======== Example73_AgentAuthoring ========"); try { // Initialize the agent with tools @@ -72,7 +58,7 @@ public static class Example73_AgentAuthoring string response = await articleGenerator.AsPlugin().InvokeAsync("Thai food is the best in the world"); // Display final result - Console.WriteLine(response); + WriteLine(response); } finally { @@ -113,7 +99,7 @@ public static class Example73_AgentAuthoring .BuildAsync()); } - private async static Task CreateResearchGeneratorAsync() + private static async Task CreateResearchGeneratorAsync() { // Initialize agent so that it may be automatically deleted. return @@ -132,4 +118,8 @@ public static class Example73_AgentAuthoring return agent; } + + public Example73_AgentAuthoring(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs index 958685d6d7..39a3556be7 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs @@ -17,13 +17,13 @@ using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Plugins.Core; using Microsoft.SemanticKernel.Plugins.Web; using Microsoft.SemanticKernel.Plugins.Web.Bing; -using NCalcPlugins; +using Xunit; +using Xunit.Abstractions; -/** - * This example shows how to use FlowOrchestrator to execute a given flow with interaction with client. - */ -// ReSharper disable once InconsistentNaming -public static class Example74_FlowOrchestrator +namespace Examples; + +// This example shows how to use FlowOrchestrator to execute a given flow with interaction with client. +public class Example74_FlowOrchestrator : BaseTest { private static readonly Flow s_flow = FlowSerializer.DeserializeFromYaml(@" name: FlowOrchestrator_Example_Flow @@ -56,11 +56,9 @@ provides: - email "); - public static Task RunAsync() + [Fact(Skip = "Can take more than 1 minute")] + public Task RunAsync() { - // Load assemblies for external plugins - Console.WriteLine("Loading {0}", typeof(SimpleCalculatorPlugin).AssemblyQualifiedName); - return RunExampleAsync(); //return RunInteractiveAsync(); } @@ -331,6 +329,10 @@ Do not expose the regex in your response. public string? Content { get; set; } } } + + public Example74_FlowOrchestrator(ITestOutputHelper output) : base(output) + { + } } //***************************************************** diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step1_Create_Kernel.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step1_Create_Kernel.cs index e752f68074..e5e72d3664 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step1_Create_Kernel.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step1_Create_Kernel.cs @@ -1,17 +1,22 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; +using Examples; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; // This example shows how to create and use a . -public static class Step1_Create_Kernel +public class Step1_Create_Kernel : BaseTest { /// /// Show how to create a and use it to execute prompts. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a kernel with OpenAI chat completion Kernel kernel = Kernel.CreateBuilder() @@ -21,23 +26,30 @@ public static class Step1_Create_Kernel .Build(); // Example 1. Invoke the kernel with a prompt and display the result - Console.WriteLine(await kernel.InvokePromptAsync("What color is the sky?")); - Console.WriteLine(); + WriteLine(await kernel.InvokePromptAsync("What color is the sky?")); + WriteLine(); // Example 2. Invoke the kernel with a templated prompt and display the result KernelArguments arguments = new() { { "topic", "sea" } }; - Console.WriteLine(await kernel.InvokePromptAsync("What color is the {{$topic}}?", arguments)); - Console.WriteLine(); + WriteLine(await kernel.InvokePromptAsync("What color is the {{$topic}}?", arguments)); + WriteLine(); // Example 3. Invoke the kernel with a templated prompt and stream the results to the display + await foreach (var update in kernel.InvokePromptStreamingAsync("What color is the {{$topic}}? Provide a detailed explanation.", arguments)) { - Console.Write(update); + Write(update); + // Console.Write(update); } - Console.WriteLine(); + + WriteLine(string.Empty); // Example 4. Invoke the kernel with a templated prompt and execution settings arguments = new(new OpenAIPromptExecutionSettings { MaxTokens = 500, Temperature = 0.5 }) { { "topic", "dogs" } }; - Console.WriteLine(await kernel.InvokePromptAsync("Tell me a story about {{$topic}}", arguments)); + WriteLine(await kernel.InvokePromptAsync("Tell me a story about {{$topic}}", arguments)); + } + + public Step1_Create_Kernel(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step2_Add_Plugins.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step2_Add_Plugins.cs index 857a709100..02f63795d2 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step2_Add_Plugins.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step2_Add_Plugins.cs @@ -3,16 +3,22 @@ using System; using System.ComponentModel; using System.Threading.Tasks; +using Examples; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; // This example shows how to load a instances. -public static class Step2_Add_Plugins +public class Step2_Add_Plugins : BaseTest { /// /// Shows different ways to load a instances. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a kernel with OpenAI chat completion IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); @@ -23,14 +29,14 @@ public static class Step2_Add_Plugins Kernel kernel = kernelBuilder.Build(); // Example 1. Invoke the kernel with a prompt that asks the AI for inromation it cannot provide and may hallucinate - Console.WriteLine(await kernel.InvokePromptAsync("How many days until Christmas?")); + WriteLine(await kernel.InvokePromptAsync("How many days until Christmas?")); // Example 2. Invoke the kernel with a templated prompt that invokes a plugin and display the result - Console.WriteLine(await kernel.InvokePromptAsync("The current time is {{TimeInformation.GetCurrentUtcTime}}. How many days until Christmas?")); + WriteLine(await kernel.InvokePromptAsync("The current time is {{TimeInformation.GetCurrentUtcTime}}. How many days until Christmas?")); // Example 3. Invoke the kernel with a prompt and allow the AI to automatically invoke functions OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - Console.WriteLine(await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings))); + WriteLine(await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings))); } /// @@ -42,4 +48,8 @@ public static class Step2_Add_Plugins [Description("Retrieves the current time in UTC.")] public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); } + + public Step2_Add_Plugins(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step3_Yaml_Prompt.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step3_Yaml_Prompt.cs index 6b70d2810f..f935a6ae15 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step3_Yaml_Prompt.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step3_Yaml_Prompt.cs @@ -1,18 +1,23 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; +using Examples; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.PromptTemplates.Handlebars; using Resources; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; // This example shows how to create a prompt from a YAML resource. -public static class Step3_Yaml_Prompt +public class Step3_Yaml_Prompt : BaseTest { /// /// Show how to create a prompt from a YAML resource. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a kernel with OpenAI chat completion Kernel kernel = Kernel.CreateBuilder() @@ -26,7 +31,7 @@ public static class Step3_Yaml_Prompt var function = kernel.CreateFunctionFromPromptYaml(generateStoryYaml); // Invoke the prompt function and display the result - Console.WriteLine(await kernel.InvokeAsync(function, arguments: new() + WriteLine(await kernel.InvokeAsync(function, arguments: new() { { "topic", "Dog" }, { "length", "3" }, @@ -37,10 +42,14 @@ public static class Step3_Yaml_Prompt function = kernel.CreateFunctionFromPromptYaml(generateStoryHandlebarsYaml, new HandlebarsPromptTemplateFactory()); // Invoke the prompt function and display the result - Console.WriteLine(await kernel.InvokeAsync(function, arguments: new() + WriteLine(await kernel.InvokeAsync(function, arguments: new() { { "topic", "Cat" }, { "length", "3" }, })); } + + public Step3_Yaml_Prompt(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step4_Dependency_Injection.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step4_Dependency_Injection.cs index ccddc6f066..0bddb8d2db 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step4_Dependency_Injection.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step4_Dependency_Injection.cs @@ -3,18 +3,24 @@ using System; using System.ComponentModel; using System.Threading.Tasks; +using Examples; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using RepoUtils; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; // This example shows how to using Dependency Injection with the Semantic Kernel -public static class Step4_Dependency_Injection +public class Step4_Dependency_Injection : BaseTest { /// /// Show how to create a that participates in Dependency Injection. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // If an application follows DI guidelines, the following line is unnecessary because DI will inject an instance of the KernelClient class to a class that references it. // DI container guidelines - https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines#recommendations @@ -25,7 +31,7 @@ public static class Step4_Dependency_Injection KernelArguments arguments = new() { { "topic", "earth when viewed from space" } }; await foreach (var update in kernel.InvokePromptStreamingAsync("What color is the {{$topic}}? Provide a detailed explanation.", arguments)) { - Console.Write(update); + Write(update); } } @@ -35,7 +41,7 @@ public static class Step4_Dependency_Injection private static ServiceProvider BuildServiceProvider() { var collection = new ServiceCollection(); - collection.AddSingleton(ConsoleLogger.LoggerFactory); + collection.AddSingleton(ConsoleLogger.LoggerFactory); var kernelBuilder = collection.AddKernel(); kernelBuilder.Services.AddOpenAITextGeneration(TestConfiguration.OpenAI.ModelId, TestConfiguration.OpenAI.ApiKey); @@ -65,4 +71,8 @@ public static class Step4_Dependency_Injection return utcNow; } } + + public Step4_Dependency_Injection(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step5_Chat_Prompt.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step5_Chat_Prompt.cs index a2db7ca0f9..697f4ae52c 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step5_Chat_Prompt.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step5_Chat_Prompt.cs @@ -1,15 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; +using Examples; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; -public static class Step5_Chat_Prompt +namespace GettingStarted; + +public class Step5_Chat_Prompt : BaseTest { /// /// Show how to construct a chat prompt and invoke it. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a kernel with OpenAI chat completion Kernel kernel = Kernel.CreateBuilder() @@ -23,6 +28,11 @@ public static class Step5_Chat_Prompt What is Seattle? Respond with JSON. "; - Console.WriteLine(await kernel.InvokePromptAsync(chatPrompt)); + + WriteLine(await kernel.InvokePromptAsync(chatPrompt)); + } + + public Step5_Chat_Prompt(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step6_Responsible_AI.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step6_Responsible_AI.cs index 7a73ea5869..8c80ad9014 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step6_Responsible_AI.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step6_Responsible_AI.cs @@ -1,16 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading.Tasks; +using Examples; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; // This example shows how to use rendering event hooks to ensure that prompts are rendered in a responsible manner. -public static class Step6_Responsible_AI +public class Step6_Responsible_AI : BaseTest { /// /// Show how to use rendering event hooks to ensure that prompts are rendered in a responsible manner. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a kernel with OpenAI chat completion Kernel kernel = Kernel.CreateBuilder() @@ -33,7 +38,7 @@ public static class Step6_Responsible_AI { e.RenderedPrompt += " NO SEXISM, RACISM OR OTHER BIAS/BIGOTRY"; - Console.WriteLine(e.RenderedPrompt); + WriteLine(e.RenderedPrompt); } // Add the handlers to the kernel @@ -41,6 +46,10 @@ public static class Step6_Responsible_AI kernel.PromptRendered += MyRenderedHandler; KernelArguments arguments = new() { { "card_number", "4444 3333 2222 1111" } }; - Console.WriteLine(await kernel.InvokePromptAsync("Tell me some useful information about this credit card number {{$card_number}}?", arguments)); + WriteLine(await kernel.InvokePromptAsync("Tell me some useful information about this credit card number {{$card_number}}?", arguments)); + } + + public Step6_Responsible_AI(ITestOutputHelper output) : base(output) + { } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs index d1c0acefc8..464e7ca80b 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs @@ -3,16 +3,22 @@ using System; using System.ComponentModel; using System.Threading.Tasks; +using Examples; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using RepoUtils; +using Xunit; +using Xunit.Abstractions; -public static class Step7_Observability +namespace GettingStarted; + +public class Step7_Observability : BaseTest { /// /// Shows different ways observe the execution of a instances. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { // Create a kernel with OpenAI chat completion IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); @@ -25,13 +31,13 @@ public static class Step7_Observability // Handler which is called before a function is invoked void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) { - Console.WriteLine($"Invoking {e.Function.Name}"); + WriteLine($"Invoking {e.Function.Name}"); } // Handler which is called after a prompt is rendered void MyRenderedHandler(object? sender, PromptRenderedEventArgs e) { - Console.WriteLine($"Prompt sent to model: {e.RenderedPrompt}"); + WriteLine($"Prompt sent to model: {e.RenderedPrompt}"); } // Handler which is called after a function is invoked @@ -39,7 +45,7 @@ public static class Step7_Observability { if (e.Result.Metadata is not null && e.Result.Metadata.ContainsKey("Usage")) { - Console.WriteLine($"Token usage: {e.Result.Metadata?["Usage"]?.AsJson()}"); + WriteLine($"Token usage: {e.Result.Metadata?["Usage"]?.AsJson()}"); } } @@ -50,7 +56,7 @@ public static class Step7_Observability // Invoke the kernel with a prompt and allow the AI to automatically invoke functions OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - Console.WriteLine(await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings))); + WriteLine(await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings))); } /// @@ -62,4 +68,8 @@ public static class Step7_Observability [Description("Retrieves the current time in UTC.")] public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); } + + public Step7_Observability(ITestOutputHelper output) : base(output) + { + } } diff --git a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step8_Pipelining.cs b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step8_Pipelining.cs index 633d850103..096310d745 100644 --- a/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step8_Pipelining.cs +++ b/dotnet/samples/KernelSyntaxExamples/Getting_Started/Step8_Pipelining.cs @@ -6,17 +6,23 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Examples; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; -public static class Step8_Pipelining +namespace GettingStarted; + +public class Step8_Pipelining : BaseTest { /// /// Provides an example of combining multiple functions into a single function that invokes /// them in a sequence, passing the output from one as input to the next. /// - public static async Task RunAsync() + [Fact] + public async Task RunAsync() { IKernelBuilder builder = Kernel.CreateBuilder(); builder.AddOpenAIChatCompletion( @@ -25,7 +31,7 @@ public static class Step8_Pipelining builder.Services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Trace)); Kernel kernel = builder.Build(); - Console.WriteLine("================ PIPELINE ================"); + WriteLine("================ PIPELINE ================"); { // Create a pipeline of functions that will parse a string into an int, multiply it by a double, truncate it to an int, and then humanize it. KernelFunction parseInt32 = KernelFunctionFactory.CreateFromMethod((string s) => double.Parse(s, CultureInfo.InvariantCulture), "parseInt32"); @@ -47,10 +53,10 @@ public static class Step8_Pipelining // - The parseInt32 function will be invoked, read "123.456" from the arguments, and parse it into (double)123.456. // - The multiplyByN function will be invoked, with i=123.456 and n=78.90, and return (double)9740.6784. // - The truncate function will be invoked, with d=9740.6784, and return (int)9740, which will be the final result. - Console.WriteLine(await pipeline.InvokeAsync(kernel, args)); + WriteLine(await pipeline.InvokeAsync(kernel, args)); } - Console.WriteLine("================ GRAPH ================"); + WriteLine("================ GRAPH ================"); { KernelFunction rand = KernelFunctionFactory.CreateFromMethod(() => Random.Shared.Next(), "GetRandomInt32"); KernelFunction mult = KernelFunctionFactory.CreateFromMethod((int i, int j) => i * j, "Multiply"); @@ -65,9 +71,13 @@ public static class Step8_Pipelining (mult, "") }, "graph"); - Console.WriteLine(await graph.InvokeAsync(kernel)); + WriteLine(await graph.InvokeAsync(kernel)); } } + + public Step8_Pipelining(ITestOutputHelper output) : base(output) + { + } } public static class KernelFunctionCombinators diff --git a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj index b16f9fde1c..48bf7195f8 100644 --- a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj +++ b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj @@ -3,19 +3,17 @@ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + KernelSyntaxExamples net6.0 LatestMajor - Exe + true false - CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102 + CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102 + Library - - - - @@ -24,6 +22,13 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -36,7 +41,6 @@ - @@ -69,7 +73,6 @@ - diff --git a/dotnet/samples/KernelSyntaxExamples/Program.cs b/dotnet/samples/KernelSyntaxExamples/Program.cs deleted file mode 100644 index 4f1b667693..0000000000 --- a/dotnet/samples/KernelSyntaxExamples/Program.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Reliability; -using RepoUtils; - -public static class Program -{ - /// - /// We recommend using the Debug properties to set the filter as a command line argument. - /// If you don't want to use it, set the filter here. - /// Examples: - /// DefaultFilter = "18" => run only example 18 (also "180" if there is such test) - /// DefaultFilter = "chat" => run all examples with a name that contains "chat" - /// - public const string? DefaultFilter = ""; - - public static async Task Main(string[] args) - { - // Load configuration from environment variables or user secrets. - LoadUserSecrets(); - - // Execution canceled if the user presses Ctrl+C. - using CancellationTokenSource cancellationTokenSource = new(); - CancellationToken cancelToken = cancellationTokenSource.ConsoleCancellationToken(); - - // Check if args[0] is provided - string? filter = args.Length > 0 ? args[0] : DefaultFilter; - - // Run examples based on the filter - await RunExamplesAsync(filter, cancelToken); - } - - private static async Task RunExamplesAsync(string? filter, CancellationToken cancellationToken) - { - var examples = (Assembly.GetExecutingAssembly().GetTypes()) - .Select(type => type.Name).ToList(); - - // Filter and run examples - foreach (var example in examples) - { - if (string.IsNullOrEmpty(filter) || example.Contains(filter, StringComparison.OrdinalIgnoreCase)) - { - try - { - var method = Assembly.GetExecutingAssembly().GetType(example)?.GetMethod("RunAsync"); - if (method == null) - { - // Skip if the type does not have a RunAsync method - continue; - } - - Console.WriteLine($"Running {example}..."); - - bool hasCancellationToken = method.GetParameters().Any(param => param.ParameterType == typeof(CancellationToken)); - - var taskParameters = hasCancellationToken ? new object[] { cancellationToken } : null; - if (method.Invoke(null, taskParameters) is Task t) - { - await t.SafeWaitAsync(cancellationToken); - } - else - { - method.Invoke(null, null); - } - } - catch (ConfigurationNotFoundException ex) - { - Console.WriteLine($"{ex.Message}. Skipping example {example}."); - } - } - } - } - - private static void LoadUserSecrets() - { - IConfigurationRoot configRoot = new ConfigurationBuilder() - .AddJsonFile("appsettings.Development.json", true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - TestConfiguration.Initialize(configRoot); - } - - private static CancellationToken ConsoleCancellationToken(this CancellationTokenSource tokenSource) - { - Console.CancelKeyPress += (s, e) => - { - Console.WriteLine("Canceling..."); - tokenSource.Cancel(); - e.Cancel = true; - }; - - return tokenSource.Token; - } - - private static async Task SafeWaitAsync(this Task task, - CancellationToken cancellationToken = default) - { - try - { - await task.WaitAsync(cancellationToken); - Console.WriteLine(); - Console.WriteLine("== DONE =="); - } - catch (ConfigurationNotFoundException ex) - { - Console.WriteLine($"{ex.Message}. Skipping example."); - } - - cancellationToken.ThrowIfCancellationRequested(); - } -} diff --git a/dotnet/samples/KernelSyntaxExamples/README.md b/dotnet/samples/KernelSyntaxExamples/README.md index 4360c4b9b8..8c8996efa3 100644 --- a/dotnet/samples/KernelSyntaxExamples/README.md +++ b/dotnet/samples/KernelSyntaxExamples/README.md @@ -103,9 +103,6 @@ dotnet user-secrets set "Google:SearchEngineId" "..." dotnet user-secrets set "Github:PAT" "github_pat_..." -dotnet user-secrets set "Apim:Endpoint" "https://apim...azure-api.net/" -dotnet user-secrets set "Apim:SubscriptionKey" "..." - dotnet user-secrets set "Postgres:ConnectionString" "..." dotnet user-secrets set "Redis:Configuration" "..." dotnet user-secrets set "Kusto:ConnectionString" "..." @@ -173,10 +170,6 @@ Google__SearchEngineId # Github Github__PAT -# Azure API Management (APIM) -Apim__Endpoint -Apim__SubscriptionKey - # Other Postgres__ConnectionString Redis__Configuration diff --git a/dotnet/samples/KernelSyntaxExamples/RepoUtils/TextOutputHelperExtensions.cs b/dotnet/samples/KernelSyntaxExamples/RepoUtils/TextOutputHelperExtensions.cs new file mode 100644 index 0000000000..965afd7604 --- /dev/null +++ b/dotnet/samples/KernelSyntaxExamples/RepoUtils/TextOutputHelperExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Xunit.Abstractions; + +namespace Examples; + +public static class TextOutputHelperExtensions +{ + public static void WriteLine(this ITestOutputHelper testOutputHelper, object target) + { + testOutputHelper.WriteLine(target.ToString()); + } + + public static void WriteLine(this ITestOutputHelper testOutputHelper) + { + testOutputHelper.WriteLine(string.Empty); + } + + public static void Write(this ITestOutputHelper testOutputHelper) + { + testOutputHelper.WriteLine(string.Empty); + } + + /// + /// Current interface ITestOutputHelper does not have a Write method. This extension method adds it to make it analogous to Console.Write when used in Console apps. + /// + /// TestOutputHelper + /// Target object to write + public static void Write(this ITestOutputHelper testOutputHelper, object target) + { + testOutputHelper.WriteLine(target.ToString()); + } +} diff --git a/dotnet/samples/NCalcPlugins/LanguageCalculatorPlugin.cs b/dotnet/samples/NCalcPlugins/LanguageCalculatorPlugin.cs deleted file mode 100644 index 20b53d4a07..0000000000 --- a/dotnet/samples/NCalcPlugins/LanguageCalculatorPlugin.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using NCalc; - -namespace NCalcPlugins; - -/// -/// Plugin that enables the comprehension of mathematical problems presented in English / natural-language text, followed by the execution of the necessary calculations to solve those problems. -/// -public class LanguageCalculatorPlugin -{ - private readonly KernelFunction _mathTranslator; - private const string MathTranslatorPrompt = - @"Translate a math problem into a expression that can be executed using .net NCalc library. Use the output of running this code to answer the question. -Available functions: Abs, Acos, Asin, Atan, Ceiling, Cos, Exp, Floor, IEEERemainder, Log, Log10, Max, Min, Pow, Round, Sign, Sin, Sqrt, Tan, and Truncate. in and if are also supported. - -Question: $((Question with math problem.)) -expression:``` $((single line mathematical expression that solves the problem))``` - -[Examples] -Question: What is 37593 * 67? -expression:```37593 * 67``` - -Question: what is 3 to the 2nd power? -expression:```Pow(3, 2)``` - -Question: what is sine of 0 radians? -expression:```Sin(0)``` - -Question: what is sine of 45 degrees? -expression:```Sin(45 * Pi /180 )``` - -Question: how many radians is 45 degrees? -expression:``` 45 * Pi / 180 ``` - -Question: what is the square root of 81? -expression:```Sqrt(81)``` - -Question: what is the angle whose sine is the number 1? -expression:```Asin(1)``` - -[End of Examples] - -Question: {{ $input }} -"; - - /// - /// Initializes a new instance of the class. - /// - public LanguageCalculatorPlugin() - { - this._mathTranslator = KernelFunctionFactory.CreateFromPrompt( - MathTranslatorPrompt, - functionName: "TranslateMathProblem", - description: "Used by 'Calculator' function.", - executionSettings: new PromptExecutionSettings() - { - ExtensionData = new Dictionary() - { - { "MaxTokens", 256 }, - { "Temperature", 0.0 }, - { "TopP", 1 }, - } - }); - } - - /// - /// Calculates the result of a non-trivial math expression. - /// - /// A valid mathematical expression that could be executed by a calculator capable of more advanced math functions like sine/cosine/floor. - /// The containing services, plugins, and other state for use throughout the operation. - /// A representing the result of the asynchronous operation. - [KernelFunction("Calculator"), Description("Useful for getting the result of a non-trivial math expression.")] - public async Task CalculateAsync( - [Description("A valid mathematical expression that could be executed by a calculator capable of more advanced math functions like sin/cosine/floor.")] - string input, - Kernel kernel) - { - string answer; - - try - { - var result = await kernel.InvokeAsync(this._mathTranslator, new() { ["input"] = input }).ConfigureAwait(false); - answer = result?.GetValue() ?? string.Empty; - } - catch (Exception ex) - { - throw new InvalidOperationException($"Error in calculator for input {input} {ex.Message}", ex); - } - - string pattern = @"```\s*(.*?)\s*```"; - - Match match = Regex.Match(answer, pattern, RegexOptions.Singleline); - if (match.Success) - { - var result = EvaluateMathExpression(match); - return result; - } - - throw new InvalidOperationException($"Input value [{input}] could not be understood, received following {answer}"); - } - - private static string EvaluateMathExpression(Match match) - { - var textExpressions = match.Groups[1].Value; - var expr = new Expression(textExpressions, EvaluateOptions.IgnoreCase); - expr.EvaluateParameter += (string name, ParameterArgs args) => - { - args.Result = name.ToLower(System.Globalization.CultureInfo.CurrentCulture) switch - { - "pi" => Math.PI, - "e" => Math.E, - _ => args.Result - }; - }; - - try - { - if (expr.HasErrors()) - { - return "Error:" + expr.Error + " could not evaluate " + textExpressions; - } - - var result = expr.Evaluate(); - return "Answer:" + result.ToString(); - } - catch (Exception e) - { - throw new InvalidOperationException("could not evaluate " + textExpressions, e); - } - } -} diff --git a/dotnet/samples/NCalcPlugins/NCalcPlugins.csproj b/dotnet/samples/NCalcPlugins/NCalcPlugins.csproj deleted file mode 100644 index 7aa023e93f..0000000000 --- a/dotnet/samples/NCalcPlugins/NCalcPlugins.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - netstandard2.0 - 10 - - - - - - - - - - diff --git a/dotnet/samples/NCalcPlugins/SimpleCalculatorPlugin.cs b/dotnet/samples/NCalcPlugins/SimpleCalculatorPlugin.cs deleted file mode 100644 index 0fa5bcacaf..0000000000 --- a/dotnet/samples/NCalcPlugins/SimpleCalculatorPlugin.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using Microsoft.SemanticKernel; - -namespace NCalcPlugins; - -/// -/// Simple calculator plugin that evaluates a mathematical expression. -/// -public class SimpleCalculatorPlugin -{ - private readonly KernelFunction _mathTranslator; - - private static readonly string[] s_stopSequences = new[] { "Problem:", "Solution:" }; - - /// - /// Initializes a new instance of the class. - /// - public SimpleCalculatorPlugin() - { - this._mathTranslator = KernelFunctionFactory.CreateFromPrompt( - "Task: Give the final solution for the problem. Be as concise as possible.\nProblem:4+4\nSolution:8\nProblem:{{$input}}\nSolution:\n", - functionName: "Calculator", - description: "Evaluate a mathematical expression. Input is a valid mathematical expression that could be executed by a simple calculator i.e. add, subtract, multiply and divide. Cannot use variables.", - executionSettings: new PromptExecutionSettings() - { - ExtensionData = new Dictionary() - { - { "MaxTokens", 256 }, - { "Temperature", 0.0 }, - { "StopSequences", s_stopSequences }, - } - }); - } -} diff --git a/dotnet/samples/TelemetryExample/TelemetryExample.csproj b/dotnet/samples/TelemetryExample/TelemetryExample.csproj index 989b359d05..ebd29174e8 100644 --- a/dotnet/samples/TelemetryExample/TelemetryExample.csproj +++ b/dotnet/samples/TelemetryExample/TelemetryExample.csproj @@ -25,7 +25,6 @@ - diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs index 91888c5073..9155050518 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs @@ -2,7 +2,6 @@ using System; using System.Net.Http; -using System.Runtime.CompilerServices; using Azure; using Azure.AI.OpenAI; using Azure.Core; @@ -98,16 +97,4 @@ internal sealed class AzureOpenAIClientCore : ClientCore this.AddAttribute(DeploymentNameKey, deploymentName); } - - /// - /// Logs Azure OpenAI action details. - /// - /// Caller member name. Populated automatically by runtime. - internal void LogActionDetails([CallerMemberName] string? callerMemberName = default) - { - if (this.Logger.IsEnabled(LogLevel.Information)) - { - this.Logger.LogInformation("Action: {Action}. Azure OpenAI Deployment Name: {DeploymentName}.", callerMemberName, this.DeploymentOrModelName); - } - } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs index 5f84d7148f..04b0e7a2c0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs @@ -30,7 +30,8 @@ public sealed class AzureOpenAIWithDataChatMessageContent : ChatMessageContent : base(default, string.Empty, modelId, chatChoice, System.Text.Encoding.UTF8, CreateMetadataDictionary(metadata)) { // An assistant message content must be present, otherwise the chat is not valid. - var chatMessage = chatChoice.Messages.First(m => string.Equals(m.Role, AuthorRole.Assistant.Label, StringComparison.OrdinalIgnoreCase)); + var chatMessage = chatChoice.Messages.FirstOrDefault(m => string.Equals(m.Role, AuthorRole.Assistant.Label, StringComparison.OrdinalIgnoreCase)) ?? + throw new ArgumentException("Chat is not valid. Chat message does not contain any messages with 'assistant' role."); this.Content = chatMessage.Content; this.Role = new AuthorRole(chatMessage.Role); diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs index 5fa88e2d4f..76c2bc9e53 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs @@ -33,13 +33,13 @@ public static class OpenAIMemoryBuilderExtensions string? modelId = null, HttpClient? httpClient = null) { - return builder.WithTextEmbeddingGeneration((loggerFactory, httpClient) => + return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => new AzureOpenAITextEmbeddingGenerationService( deploymentName, endpoint, apiKey, modelId, - HttpClientProvider.GetHttpClient(httpClient), + HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), loggerFactory)); } @@ -63,13 +63,13 @@ public static class OpenAIMemoryBuilderExtensions string? modelId = null, HttpClient? httpClient = null) { - return builder.WithTextEmbeddingGeneration((loggerFactory, httpClient) => + return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => new AzureOpenAITextEmbeddingGenerationService( deploymentName, endpoint, credential, modelId, - HttpClientProvider.GetHttpClient(httpClient), + HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), loggerFactory)); } @@ -91,12 +91,12 @@ public static class OpenAIMemoryBuilderExtensions string? orgId = null, HttpClient? httpClient = null) { - return builder.WithTextEmbeddingGeneration((loggerFactory, httpClient) => + return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => new OpenAITextEmbeddingGenerationService( modelId, apiKey, orgId, - HttpClientProvider.GetHttpClient(httpClient), + HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), loggerFactory)); } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs index f1277a3dfd..381c86c6aa 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs @@ -93,7 +93,6 @@ public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGe Kernel? kernel = null, CancellationToken cancellationToken = default) { - this._core.LogActionDetails(); return this._core.GetEmbeddingsAsync(data, kernel, cancellationToken); } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs index 72531434ca..837b877845 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text.Json; @@ -87,10 +86,7 @@ public sealed class OpenAITextToImageService : ITextToImageService string format, Func extractResponse, CancellationToken cancellationToken) { - Debug.Assert(width == height); - Debug.Assert(width is 256 or 512 or 1024); - Debug.Assert(format is "url" or "b64_json"); - Debug.Assert(extractResponse is not null); + Verify.NotNull(extractResponse); var requestBody = JsonSerializer.Serialize(new TextToImageRequest { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj index 1c7a297872..a541b834ba 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj @@ -27,14 +27,17 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + @@ -55,16 +58,10 @@ - + Always - - Always - - - Always - - + Always diff --git a/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs b/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs new file mode 100644 index 0000000000..f83ac864d0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace SemanticKernel.Connectors.UnitTests; + +internal sealed class MultipleHttpMessageHandlerStub : DelegatingHandler +{ + private int _callIteration = 0; + + public List RequestHeaders { get; private set; } + + public List ContentHeaders { get; private set; } + + public List RequestContents { get; private set; } + + public List RequestUris { get; private set; } + + public List Methods { get; private set; } + + public List ResponsesToReturn { get; set; } + + public MultipleHttpMessageHandlerStub() + { + this.RequestHeaders = []; + this.ContentHeaders = []; + this.RequestContents = []; + this.RequestUris = []; + this.Methods = []; + this.ResponsesToReturn = []; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this._callIteration++; + + this.Methods.Add(request.Method); + this.RequestUris.Add(request.RequestUri); + this.RequestHeaders.Add(request.Headers); + this.ContentHeaders.Add(request.Content?.Headers); + + var content = request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken); + + this.RequestContents.Add(content); + + return await Task.FromResult(this.ResponsesToReturn[this._callIteration - 1]); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs new file mode 100644 index 0000000000..76d5c6b7d8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIWithDataChatMessageContentTests +{ + [Fact] + public void ConstructorThrowsExceptionWhenAssistantMessageIsNotProvided() + { + // Arrange + var choice = new ChatWithDataChoice(); + + // Act & Assert + var exception = Assert.Throws(() => new AzureOpenAIWithDataChatMessageContent(choice, "model-id")); + + Assert.Contains("Chat is not valid", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ConstructorReturnsInstanceWithNullToolContent() + { + // Arrange + var choice = new ChatWithDataChoice { Messages = [new() { Content = "Assistant content", Role = "assistant" }] }; + + // Act + var content = new AzureOpenAIWithDataChatMessageContent(choice, "model-id"); + + // Assert + Assert.Equal("Assistant content", content.Content); + Assert.Equal(AuthorRole.Assistant, content.Role); + + Assert.Null(content.ToolContent); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorReturnsInstanceWithNonNullToolContent(bool includeMetadata) + { + // Arrange + var choice = new ChatWithDataChoice + { + Messages = [ + new() { Content = "Assistant content", Role = "assistant" }, + new() { Content = "Tool content", Role = "tool" }] + }; + + // Act + var content = includeMetadata ? + new AzureOpenAIWithDataChatMessageContent(choice, "model-id", new Dictionary()) : + new AzureOpenAIWithDataChatMessageContent(choice, "model-id"); + + // Assert + Assert.Equal("Assistant content", content.Content); + Assert.Equal("Tool content", content.ToolContent); + Assert.Equal(AuthorRole.Assistant, content.Role); + + Assert.NotNull(content.Metadata); + Assert.Equal("Tool content", content.Metadata["ToolContent"]); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs new file mode 100644 index 0000000000..d3fd86820e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIWithDataStreamingChatMessageContentTests +{ + [Theory] + [MemberData(nameof(ValidChoices))] + public void ConstructorWithValidChoiceSetsNonEmptyContent(object choice, string expectedContent) + { + // Arrange + var streamingChoice = choice as ChatWithDataStreamingChoice; + + // Act + var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id"); + + // Assert + Assert.Equal(expectedContent, content.Content); + } + + [Theory] + [MemberData(nameof(InvalidChoices))] + public void ConstructorWithInvalidChoiceSetsNullContent(object choice) + { + // Arrange + var streamingChoice = choice as ChatWithDataStreamingChoice; + + // Act + var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id"); + + // Assert + Assert.Null(content.Content); + } + + public static IEnumerable ValidChoices + { + get + { + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content 1" } }] }, "Content 1" }; + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content 2", Role = "Assistant" } }] }, "Content 2" }; + } + } + + public static IEnumerable InvalidChoices + { + get + { + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { EndTurn = true }] } }; + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content", Role = "tool" } }] } }; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs new file mode 100644 index 0000000000..6f7f271b3c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIChatMessageContentTests +{ + [Fact] + public void ConstructorsWorkCorrectly() + { + // Arrange + List toolCalls = [new FakeChatCompletionsToolCall("id")]; + + // Act + var content1 = new OpenAIChatMessageContent(new ChatRole("user"), "content1", "model-id1", toolCalls); + var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); + + // Assert + this.AssertChatMessageContent(AuthorRole.User, "content1", "model-id1", toolCalls, content1); + this.AssertChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, content2); + } + + [Fact] + public void GetOpenAIFunctionToolCallsReturnsCorrectList() + { + // Arrange + List toolCalls = [ + new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), + new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), + new FakeChatCompletionsToolCall("id3"), + new FakeChatCompletionsToolCall("id4")]; + + var content1 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); + var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); + + // Act + var actualToolCalls1 = content1.GetOpenAIFunctionToolCalls(); + var actualToolCalls2 = content2.GetOpenAIFunctionToolCalls(); + + // Assert + Assert.Equal(2, actualToolCalls1.Count); + Assert.Equal("id1", actualToolCalls1[0].Id); + Assert.Equal("id2", actualToolCalls1[1].Id); + + Assert.Empty(actualToolCalls2); + } + + [Fact] + public void MetadataIsInitializedCorrectly() + { + // Arrange + var metadata = new Dictionary { { "key", "value" } }; + List toolCalls = [ + new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), + new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), + new FakeChatCompletionsToolCall("id3"), + new FakeChatCompletionsToolCall("id4")]; + + // Act + var content1 = new OpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); + var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, metadata); + + // Assert + Assert.NotNull(content1.Metadata); + Assert.Single(content1.Metadata); + + Assert.NotNull(content2.Metadata); + Assert.Equal(2, content2.Metadata.Count); + Assert.Equal("value", content2.Metadata["key"]); + + Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); + + var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; + Assert.NotNull(actualToolCalls); + + Assert.Equal(2, actualToolCalls.Count); + Assert.Equal("id1", actualToolCalls[0].Id); + Assert.Equal("id2", actualToolCalls[1].Id); + } + + private void AssertChatMessageContent( + AuthorRole expectedRole, + string expectedContent, + string expectedModelId, + IReadOnlyList expectedToolCalls, + OpenAIChatMessageContent actualContent) + { + Assert.Equal(expectedRole, actualContent.Role); + Assert.Equal(expectedContent, actualContent.Content); + Assert.Equal(expectedModelId, actualContent.ModelId); + Assert.Same(expectedToolCalls, actualContent.ToolCalls); + } + + private sealed class FakeChatCompletionsToolCall(string id) : ChatCompletionsToolCall(id) + { } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs new file mode 100644 index 0000000000..9b4d53adb1 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIFunctionToolCallTests +{ + [Theory] + [InlineData("MyFunction", "MyFunction")] + [InlineData("MyPlugin_MyFunction", "MyPlugin_MyFunction")] + public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) + { + // Arrange + var toolCall = new ChatCompletionsFunctionToolCall("id", toolCallName, string.Empty); + var openAIFunctionToolCall = new OpenAIFunctionToolCall(toolCall); + + // Act & Assert + Assert.Equal(expectedName, openAIFunctionToolCall.FullyQualifiedName); + } + + [Fact] + public void ToStringReturnsCorrectValue() + { + // Arrange + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); + var openAIFunctionToolCall = new OpenAIFunctionToolCall(toolCall); + + // Act & Assert + Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", openAIFunctionToolCall.ToString()); + } + + [Fact] + public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() + { + // Arrange + var toolCallIdsByIndex = new Dictionary(); + var functionNamesByIndex = new Dictionary(); + var functionArgumentBuildersByIndex = new Dictionary(); + + // Act + var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + ref toolCallIdsByIndex, + ref functionNamesByIndex, + ref functionArgumentBuildersByIndex); + + // Assert + Assert.Empty(toolCalls); + } + + [Fact] + public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() + { + // Arrange + var toolCallIdsByIndex = new Dictionary { { 3, "test-id" } }; + var functionNamesByIndex = new Dictionary { { 3, "test-function" } }; + var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; + + // Act + var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + ref toolCallIdsByIndex, + ref functionNamesByIndex, + ref functionArgumentBuildersByIndex); + + // Assert + Assert.Single(toolCalls); + + var toolCall = toolCalls[0]; + + Assert.Equal("test-id", toolCall.Id); + Assert.Equal("test-function", toolCall.Name); + Assert.Equal("test-argument", toolCall.Arguments); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs new file mode 100644 index 0000000000..ec535ba2b4 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIPluginCollectionExtensionsTests +{ + [Fact] + public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() + { + // Arrange + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", []); + var plugins = new KernelPluginCollection([plugin]); + + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); + + // Act + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.False(result); + Assert.Null(actualFunction); + Assert.Null(actualArguments); + } + + [Fact] + public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + + var plugins = new KernelPluginCollection([plugin]); + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); + + // Act + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.True(result); + Assert.Same(function, actualFunction); + Assert.Null(actualArguments); + } + + [Fact] + public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + + var plugins = new KernelPluginCollection([plugin]); + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); + + // Act + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.True(result); + Assert.Same(function, actualFunction); + + Assert.NotNull(actualArguments); + Assert.Equal("San Diego", actualArguments["location"]); + Assert.Equal("300", actualArguments["max_price"]); + Assert.Null(actualArguments["null_argument"]); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs new file mode 100644 index 0000000000..fd0a830cc2 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIStreamingTextContentTests +{ + [Fact] + public void ToByteArrayWorksCorrectly() + { + // Arrange + var expectedBytes = Encoding.UTF8.GetBytes("content"); + var content = new OpenAIStreamingTextContent("content", 0, "model-id"); + + // Act + var actualBytes = content.ToByteArray(); + + // Assert + Assert.Equal(expectedBytes, actualBytes); + } + + [Theory] + [InlineData(null, "")] + [InlineData("content", "content")] + public void ToStringWorksCorrectly(string? content, string expectedString) + { + // Arrange + var textContent = new OpenAIStreamingTextContent(content!, 0, "model-id"); + + // Act + var actualString = textContent.ToString(); + + // Assert + Assert.Equal(expectedString, actualString); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs new file mode 100644 index 0000000000..4267c57435 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Azure; +using Azure.Core; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; + +/// +/// Unit tests for class. +/// +public sealed class RequestFailedExceptionExtensionsTests +{ + [Theory] + [InlineData(0, null)] + [InlineData(500, HttpStatusCode.InternalServerError)] + public void ToHttpOperationExceptionWithStatusReturnsValidException(int responseStatus, HttpStatusCode? httpStatusCode) + { + // Arrange + var exception = new RequestFailedException(responseStatus, "Error Message"); + + // Act + var actualException = exception.ToHttpOperationException(); + + // Assert + Assert.IsType(actualException); + Assert.Equal(httpStatusCode, actualException.StatusCode); + Assert.Equal("Error Message", actualException.Message); + Assert.Same(exception, actualException.InnerException); + } + + [Fact] + public void ToHttpOperationExceptionWithContentReturnsValidException() + { + // Arrange + using var response = new FakeResponse("Response Content", 500); + var exception = new RequestFailedException(response); + + // Act + var actualException = exception.ToHttpOperationException(); + + // Assert + Assert.IsType(actualException); + Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); + Assert.Equal("Response Content", actualException.ResponseContent); + Assert.Same(exception, actualException.InnerException); + } + + #region private + + private sealed class FakeResponse(string responseContent, int status) : Response + { + private readonly string _responseContent = responseContent; + private readonly int _status = status; + private readonly IEnumerable _headers = new List(); + + public override BinaryData Content => BinaryData.FromString(this._responseContent); + public override int Status => this._status; + public override string ReasonPhrase => "Reason Phrase"; + public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } + public override string ClientRequestId { get => "Client Request Id"; set => throw new NotImplementedException(); } + + public override void Dispose() { } + protected override bool ContainsHeader(string name) => throw new NotImplementedException(); + protected override IEnumerable EnumerateHeaders() => this._headers; +#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). + protected override bool TryGetHeader(string name, out string? value) => throw new NotImplementedException(); + protected override bool TryGetHeaderValues(string name, out IEnumerable? values) => throw new NotImplementedException(); +#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs new file mode 100644 index 0000000000..9374dd6f72 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -0,0 +1,583 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletion; + +/// +/// Unit tests for +/// +public sealed class AzureOpenAIChatCompletionServiceTests : IDisposable +{ + private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAIChatCompletionServiceTests() + { + this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + + var mockLogger = new Mock(); + + mockLogger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(l => l.CreateLogger(It.IsAny())).Returns(mockLogger.Object); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", credentials, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionService("deployment", client, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task GetTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetTextContentsAsync("Prompt"); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Text); + + var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetChatMessageContentsWithEmptyChoicesThrowsExceptionAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"id\":\"response-id\",\"object\":\"chat.completion\",\"created\":1704208954,\"model\":\"gpt-4\",\"choices\":[],\"usage\":{\"prompt_tokens\":55,\"completion_tokens\":100,\"total_tokens\":155},\"system_fingerprint\":null}") + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([])); + + Assert.Equal("Chat completions not found", exception.Message); + } + + [Theory] + [InlineData(0)] + [InlineData(129)] + public async Task GetChatMessageContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new OpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([], settings)); + + Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new OpenAIPromptExecutionSettings() + { + MaxTokens = 123, + Temperature = 0.6, + TopP = 0.5, + FrequencyPenalty = 1.6, + PresencePenalty = 1.2, + ResultsPerPrompt = 5, + Seed = 567, + TokenSelectionBiases = new Dictionary { { 2, 3 } }, + StopSequences = ["stop_sequence"] + }; + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("User Message"); + chatHistory.AddSystemMessage("System Message"); + chatHistory.AddAssistantMessage("Assistant Message"); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetChatMessageContentsAsync(chatHistory, settings); + + // Assert + var requestContent = this._messageHandlerStub.RequestContents[0]; + + Assert.NotNull(requestContent); + + var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); + + var messages = content.GetProperty("messages"); + + var userMessage = messages[0]; + var systemMessage = messages[1]; + var assistantMessage = messages[2]; + + Assert.Equal("user", userMessage.GetProperty("role").GetString()); + Assert.Equal("User Message", userMessage.GetProperty("content").GetString()); + + Assert.Equal("system", systemMessage.GetProperty("role").GetString()); + Assert.Equal("System Message", systemMessage.GetProperty("content").GetString()); + + Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("Assistant Message", assistantMessage.GetProperty("content").GetString()); + + Assert.Equal(123, content.GetProperty("max_tokens").GetInt32()); + Assert.Equal(0.6, content.GetProperty("temperature").GetDouble()); + Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); + Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); + Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); + Assert.Equal(5, content.GetProperty("n").GetInt32()); + Assert.Equal(567, content.GetProperty("seed").GetInt32()); + Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); + Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); + } + + [Theory] + [MemberData(nameof(ResponseFormats))] + public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(object responseFormat, string? expectedResponseType) + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new OpenAIPromptExecutionSettings + { + ResponseFormat = responseFormat + }; + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetChatMessageContentsAsync([], settings); + + // Assert + var requestContent = this._messageHandlerStub.RequestContents[0]; + + Assert.NotNull(requestContent); + + var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); + + Assert.Equal(expectedResponseType, content.GetProperty("response_format").GetProperty("type").GetString()); + } + + [Theory] + [MemberData(nameof(ToolCallBehaviors))] + public async Task GetChatMessageContentsWorksCorrectlyAsync(ToolCallBehavior behavior) + { + // Arrange + var kernel = Kernel.CreateBuilder().Build(); + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = behavior }; + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Content); + + var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetChatMessageContentsWithFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function1 = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => + { + functionCallCount++; + throw new ArgumentException("Some exception"); + }, "FunctionWithException"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Content); + + Assert.Equal(2, functionCallCount); + } + + [Fact] + public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() + { + // Arrange + const int DefaultMaximumAutoInvokeAttempts = 5; + const int AutoInvokeResponsesCount = 6; + + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var responses = new List(); + + for (var i = 0; i < AutoInvokeResponsesCount; i++) + { + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }); + } + + this._messageHandlerStub.ResponsesToReturn = responses; + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); + } + + [Fact] + public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); + + kernel.Plugins.Add(plugin); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.Equal(1, functionCallCount); + + var requestContents = this._messageHandlerStub.RequestContents; + + Assert.Equal(2, requestContents.Count); + + requestContents.ForEach(Assert.NotNull); + + var firstContent = Encoding.UTF8.GetString(requestContents[0]!); + var secondContent = Encoding.UTF8.GetString(requestContents[1]!); + + var firstContentJson = JsonSerializer.Deserialize(firstContent); + var secondContentJson = JsonSerializer.Deserialize(secondContent); + + Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("MyPlugin_GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); + } + + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }); + + // Act & Assert + await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) + { + Assert.Equal("Test chat streaming response", chunk.Text); + } + } + + [Fact] + public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }); + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([])) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function1 = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => + { + functionCallCount++; + throw new ArgumentException("Some exception"); + }, "FunctionWithException"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_multiple_function_calls_test_response.txt")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([], settings, kernel)) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + + Assert.Equal(2, functionCallCount); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() + { + // Arrange + const int DefaultMaximumAutoInvokeAttempts = 5; + const int AutoInvokeResponsesCount = 6; + + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var responses = new List(); + + for (var i = 0; i < AutoInvokeResponsesCount; i++) + { + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }); + } + + this._messageHandlerStub.ResponsesToReturn = responses; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([], settings, kernel)) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + + Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); + + kernel.Plugins.Add(plugin); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([], settings, kernel)) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + + Assert.Equal(1, functionCallCount); + + var requestContents = this._messageHandlerStub.RequestContents; + + Assert.Equal(2, requestContents.Count); + + requestContents.ForEach(Assert.NotNull); + + var firstContent = Encoding.UTF8.GetString(requestContents[0]!); + var secondContent = Encoding.UTF8.GetString(requestContents[1]!); + + var firstContentJson = JsonSerializer.Deserialize(firstContent); + var secondContentJson = JsonSerializer.Deserialize(secondContent); + + Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("MyPlugin_GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + public static TheoryData ToolCallBehaviors => new() + { + ToolCallBehavior.EnableKernelFunctions, + ToolCallBehavior.AutoInvokeKernelFunctions + }; + + public static TheoryData ResponseFormats => new() + { + { new FakeChatCompletionsResponseFormat(), null }, + { "json_object", "json_object" }, + { "text", "text" } + }; + + private sealed class FakeChatCompletionsResponseFormat : ChatCompletionsResponseFormat { } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs similarity index 80% rename from dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionTests.cs rename to dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index c693c8cd36..641339ee37 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -3,14 +3,19 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.TextGeneration; +using Moq; using Xunit; namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletion; @@ -18,17 +23,19 @@ namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletion; /// /// Unit tests for /// -public sealed class OpenAIChatCompletionTests : IDisposable +public sealed class OpenAIChatCompletionServiceTests : IDisposable { private readonly HttpMessageHandlerStub _messageHandlerStub; private readonly HttpClient _httpClient; private readonly OpenAIFunction _timepluginDate, _timepluginNow; private readonly OpenAIPromptExecutionSettings _executionSettings; + private readonly Mock _mockLoggerFactory; - public OpenAIChatCompletionTests() + public OpenAIChatCompletionServiceTests() { this._messageHandlerStub = new HttpMessageHandlerStub(); this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); IList functions = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[] { @@ -45,6 +52,37 @@ public sealed class OpenAIChatCompletionTests : IDisposable }; } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAIChatCompletionService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIChatCompletionService("model-id", "api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new OpenAIChatCompletionService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIChatCompletionService("model-id", client); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + [Fact] public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoAsync() { @@ -163,6 +201,44 @@ public sealed class OpenAIChatCompletionTests : IDisposable Assert.Equal("gpt-3.5-turbo", textContent.ModelId); } + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAIChatCompletionService("model-id", "api-key", "organization", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) + { + Assert.Equal("Test chat streaming response", chunk.Text); + } + } + + [Fact] + public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAIChatCompletionService("model-id", "api-key", "organization", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([])) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + } + [Fact] public async Task ItAddsSystemMessageAsync() { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs index 78d30786f8..485e04e3b8 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.IO; +using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; using Xunit; namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletionWithData; @@ -19,6 +22,7 @@ public sealed class AzureOpenAIChatCompletionWithDataTests : IDisposable private readonly HttpMessageHandlerStub _messageHandlerStub; private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; public AzureOpenAIChatCompletionWithDataTests() { @@ -26,6 +30,22 @@ public sealed class AzureOpenAIChatCompletionWithDataTests : IDisposable this._messageHandlerStub = new HttpMessageHandlerStub(); this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient, this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); + + // Assert + Assert.NotNull(service); + Assert.Equal("fake-completion-model-id", service.Attributes["ModelId"]); } [Fact] @@ -33,10 +53,10 @@ public sealed class AzureOpenAIChatCompletionWithDataTests : IDisposable { // Arrange const string ExpectedUri = "https://fake-completion-endpoint/openai/deployments/fake-completion-model-id/extensions/chat/completions?api-version=fake-api-version"; - var chatCompletion = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); + var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); // Act - await chatCompletion.GetChatMessageContentsAsync(new ChatHistory()); + await service.GetChatMessageContentsAsync([]); // Assert var actualUri = this._messageHandlerStub.RequestUri?.AbsoluteUri; @@ -58,10 +78,10 @@ public sealed class AzureOpenAIChatCompletionWithDataTests : IDisposable var config = this.GetConfig(); config.CompletionApiVersion = string.Empty; - var chatCompletion = new AzureOpenAIChatCompletionWithDataService(config, this._httpClient); + var service = new AzureOpenAIChatCompletionWithDataService(config, this._httpClient); // Act - await chatCompletion.GetChatMessageContentsAsync(new ChatHistory()); + await service.GetChatMessageContentsAsync([]); // Assert var actualUri = this._messageHandlerStub.RequestUri?.AbsoluteUri; @@ -69,6 +89,94 @@ public sealed class AzureOpenAIChatCompletionWithDataTests : IDisposable Assert.Contains("2023-06-01-preview", actualUri, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task GetChatMessageContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_with_data_test_response.json")) + }; + + // Act + var result = await service.GetChatMessageContentsAsync([]); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat with data response", result[0].Content); + + var usage = result[0].Metadata?["Usage"] as ChatWithDataUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_with_data_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([])) + { + Assert.Equal("Test chat with data streaming response", chunk.Content); + } + } + + [Fact] + public async Task GetTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_with_data_test_response.json")) + }; + + // Act + var result = await service.GetTextContentsAsync("Prompt"); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat with data response", result[0].Text); + + var usage = result[0].Metadata?["Usage"] as ChatWithDataUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_with_data_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) + { + Assert.Equal("Test chat with data streaming response", chunk.Text); + } + } + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs index 841bde8a32..ea25c66fdf 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs @@ -13,6 +13,38 @@ namespace SemanticKernel.Connectors.UnitTests.OpenAI.FunctionCalling; public sealed class OpenAIFunctionTests { + [Theory] + [InlineData(null, null, "", "")] + [InlineData("name", "description", "name", "description")] + public void ItInitializesOpenAIFunctionParameterCorrectly(string? name, string? description, string expectedName, string expectedDescription) + { + // Arrange & Act + var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); + var functionParameter = new OpenAIFunctionParameter(name, description, true, typeof(string), schema); + + // Assert + Assert.Equal(expectedName, functionParameter.Name); + Assert.Equal(expectedDescription, functionParameter.Description); + Assert.True(functionParameter.IsRequired); + Assert.Equal(typeof(string), functionParameter.ParameterType); + Assert.Same(schema, functionParameter.Schema); + } + + [Theory] + [InlineData(null, "")] + [InlineData("description", "description")] + public void ItInitializesOpenAIFunctionReturnParameterCorrectly(string? description, string expectedDescription) + { + // Arrange & Act + var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); + var functionParameter = new OpenAIFunctionReturnParameter(description, typeof(string), schema); + + // Assert + Assert.Equal(expectedDescription, functionParameter.Description); + Assert.Equal(typeof(string), functionParameter.ParameterType); + Assert.Same(schema, functionParameter.Schema); + } + [Fact] public void ItCanConvertToFunctionDefinitionWithNoPluginName() { @@ -27,6 +59,19 @@ public sealed class OpenAIFunctionTests Assert.Equal(sut.Description, result.Description); } + [Fact] + public void ItCanConvertToFunctionDefinitionWithNullParameters() + { + // Arrange + OpenAIFunction sut = new("plugin", "function", "description", null, null); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.Parameters.ToString()); + } + [Fact] public void ItCanConvertToFunctionDefinitionWithPluginName() { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs new file mode 100644 index 0000000000..08bde153aa --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.Core; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Memory; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIMemoryBuilderExtensionsTests +{ + private readonly Mock _mockMemoryStore = new(); + + [Fact] + public void AzureOpenAITextEmbeddingGenerationWithApiKeyWorksCorrectly() + { + // Arrange + var builder = new MemoryBuilder(); + + // Act + var memory = builder + .WithAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key", "model-id") + .WithMemoryStore(this._mockMemoryStore.Object) + .Build(); + + // Assert + Assert.NotNull(memory); + } + + [Fact] + public void AzureOpenAITextEmbeddingGenerationWithTokenCredentialWorksCorrectly() + { + // Arrange + var builder = new MemoryBuilder(); + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + + // Act + var memory = builder + .WithAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials, "model-id") + .WithMemoryStore(this._mockMemoryStore.Object) + .Build(); + + // Assert + Assert.NotNull(memory); + } + + [Fact] + public void OpenAITextEmbeddingGenerationWithApiKeyWorksCorrectly() + { + // Arrange + var builder = new MemoryBuilder(); + + // Act + var memory = builder + .WithOpenAITextEmbeddingGeneration("model-id", "api-key", "organization-id") + .WithMemoryStore(this._mockMemoryStore.Object) + .Build(); + + // Assert + Assert.NotNull(memory); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs index b4753312eb..1cdee512ad 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs @@ -158,6 +158,18 @@ public class OpenAIPromptExecutionSettingsTests AssertExecutionSettings(executionSettings); } + [Theory] + [InlineData("", "Assistant is a large language model.")] + [InlineData("System prompt", "System prompt")] + public void ItUsesCorrectChatSystemPrompt(string chatSystemPrompt, string expectedChatSystemPrompt) + { + // Arrange & Act + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = chatSystemPrompt }; + + // Assert + Assert.Equal(expectedChatSystemPrompt, settings.ChatSystemPrompt); + } + private static void AssertExecutionSettings(OpenAIPromptExecutionSettings executionSettings) { Assert.NotNull(executionSettings); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..0d263e9365 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToImage; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIServiceCollectionExtensionsTests : IDisposable +{ + private readonly HttpClient _httpClient; + + public OpenAIServiceCollectionExtensionsTests() + { + this._httpClient = new HttpClient(); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddAzureOpenAITextGenerationAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.AddAzureOpenAITextGeneration("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAITextGeneration("deployment-name"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void ServiceCollectionAddAzureOpenAITextGenerationAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAITextGeneration("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAITextGeneration("deployment-name"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddOpenAITextGenerationAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddOpenAITextGeneration("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.AddOpenAITextGeneration("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAITextGeneration("model-id"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAITextGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void ServiceCollectionAddOpenAITextGenerationAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddOpenAITextGeneration("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.Services.AddOpenAITextGeneration("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAITextGeneration("model-id"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAITextGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextEmbeddingGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void ServiceCollectionAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextEmbeddingGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddOpenAITextEmbeddingGeneration("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.AddOpenAITextEmbeddingGeneration("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAITextEmbeddingGeneration("model-id"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAITextEmbeddingGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void ServiceCollectionAddOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddOpenAITextEmbeddingGeneration("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.Services.AddOpenAITextEmbeddingGeneration("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAITextEmbeddingGeneration("model-id"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAITextEmbeddingGenerationService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + [InlineData(InitializationType.ChatCompletionWithData)] + public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new OpenAIClient("key"); + var config = this.GetCompletionWithDataConfig(); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.AddAzureOpenAIChatCompletion("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAIChatCompletion("deployment-name"), + InitializationType.ChatCompletionWithData => builder.AddAzureOpenAIChatCompletion(config), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + + if (type == InitializationType.ChatCompletionWithData) + { + Assert.True(service is AzureOpenAIChatCompletionWithDataService); + } + else + { + Assert.True(service is AzureOpenAIChatCompletionService); + } + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + [InlineData(InitializationType.ChatCompletionWithData)] + public void ServiceCollectionAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new OpenAIClient("key"); + var config = this.GetCompletionWithDataConfig(); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAIChatCompletion("deployment-name"), + InitializationType.ChatCompletionWithData => builder.Services.AddAzureOpenAIChatCompletion(config), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + + if (type == InitializationType.ChatCompletionWithData) + { + Assert.True(service is AzureOpenAIChatCompletionWithDataService); + } + else + { + Assert.True(service is AzureOpenAIChatCompletionService); + } + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddOpenAIChatCompletion("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.AddOpenAIChatCompletion("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAIChatCompletion("model-id"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAIChatCompletionService); + } + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void ServiceCollectionAddOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddOpenAIChatCompletion("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.Services.AddOpenAIChatCompletion("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAIChatCompletion("model-id"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAIChatCompletionService); + } + + [Fact] + public void KernelBuilderAddAzureOpenAITextToImageAddsValidService() + { + // Arrange + var builder = Kernel.CreateBuilder(); + + // Act + builder = builder.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", "api-key"); + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextToImageService); + } + + [Fact] + public void ServiceCollectionAddAzureOpenAITextToImageAddsValidService() + { + // Arrange + var builder = Kernel.CreateBuilder(); + + // Act + builder.Services.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", "api-key"); + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextToImageService); + } + + [Fact] + public void KernelBuilderAddOpenAITextToImageAddsValidService() + { + // Arrange + var builder = Kernel.CreateBuilder(); + + // Act + builder = builder.AddOpenAITextToImage("model-id", "api-key"); + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAITextToImageService); + } + + [Fact] + public void ServiceCollectionAddOpenAITextToImageAddsValidService() + { + // Arrange + var builder = Kernel.CreateBuilder(); + + // Act + builder.Services.AddOpenAITextToImage("model-id", "api-key"); + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is OpenAITextToImageService); + } + + public void Dispose() + { + this._httpClient.Dispose(); + } + + public enum InitializationType + { + ApiKey, + TokenCredential, + OpenAIClientInline, + OpenAIClientInServiceProvider, + ChatCompletionWithData + } + + private AzureOpenAIChatCompletionWithDataConfig GetCompletionWithDataConfig() + { + return new() + { + CompletionApiKey = "completion-api-key", + CompletionApiVersion = "completion-v1", + CompletionEndpoint = "https://completion-endpoint", + CompletionModelId = "completion-model-id", + DataSourceApiKey = "data-source-api-key", + DataSourceEndpoint = "https://data-source-endpoint", + DataSourceIndex = "data-source-index" + }; + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json new file mode 100644 index 0000000000..9761696b43 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json @@ -0,0 +1,56 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin_GetCurrentWeather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + }, + { + "id": "2", + "type": "function", + "function": { + "name": "MyPlugin_FunctionWithException", + "arguments": "{\n\"argument\": \"value\"\n}" + } + }, + { + "id": "3", + "type": "function", + "function": { + "name": "MyPlugin_NonExistentFunction", + "arguments": "{\n\"argument\": \"value\"\n}" + } + }, + { + "id": "4", + "type": "function", + "function": { + "name": "MyPlugin_InvalidArguments", + "arguments": "invalid_arguments_format" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json new file mode 100644 index 0000000000..4712f85226 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json @@ -0,0 +1,32 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin_GetCurrentWeather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt new file mode 100644 index 0000000000..17ce94647f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt @@ -0,0 +1,9 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin_GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"2","type":"function","function":{"name":"MyPlugin_FunctionWithException","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":2,"id":"3","type":"function","function":{"name":"MyPlugin_NonExistentFunction","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":3,"id":"4","type":"function","function":{"name":"MyPlugin_InvalidArguments","arguments":"invalid_arguments_format"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt new file mode 100644 index 0000000000..ee4d5894e6 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt @@ -0,0 +1,3 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin_GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt new file mode 100644 index 0000000000..8301463c60 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt @@ -0,0 +1,3 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Test chat streaming response"},"finish_reason":null}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json new file mode 100644 index 0000000000..b601bac8b5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json @@ -0,0 +1,22 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1704208954, + "model": "gpt-4", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Test chat response" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + }, + "system_fingerprint": null +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt new file mode 100644 index 0000000000..5e17403da9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt @@ -0,0 +1 @@ +data: {"id":"response-id","model":"","created":1684304924,"object":"chat.completion","choices":[{"index":0,"messages":[{"delta":{"role":"assistant","content":"Test chat with data streaming response"},"end_turn":false}]}]} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json new file mode 100644 index 0000000000..40d769dac8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json @@ -0,0 +1,28 @@ +{ + "id": "response-id", + "model": "", + "created": 1684304924, + "object": "chat.completion", + "choices": [ + { + "index": 0, + "messages": [ + { + "role": "tool", + "content": "{\"citations\": [{\"content\": \"\\nAzure AI services are cloud-based artificial intelligence (AI) services...\", \"id\": null, \"title\": \"What is Azure AI services\", \"filepath\": null, \"url\": null, \"metadata\": {\"chunking\": \"original document size=250. Scores=0.4314117431640625 and 1.72564697265625.Org Highlight count=4.\"}, \"chunk_id\": \"0\"}], \"intent\": \"[\\\"Learn about Azure AI services.\\\"]\"}", + "end_turn": false + }, + { + "role": "assistant", + "content": "Test chat with data response", + "end_turn": true + } + ] + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_generation_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_generation_test_response.json deleted file mode 100644 index 87b9ab7d7c..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_generation_test_response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": "32ba9f77-d620-4b6c-9265-ad50cb314a5c", - "status": "notRunning" -} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_result_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_result_test_response.json deleted file mode 100644 index 61904f1b0a..0000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/image_result_test_response.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "created": 1686192127, - "expires": 1686278532, - "id": "32ba9f77-d620-4b6c-9265-ad50cb314a5c", - "result": { - "created": 1686192127, - "data": [ - { "url": "https://dalleproduse.blob.core.windows.net/private/images/generated_00.png" } - ] - }, - "status": "succeeded" -} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt new file mode 100644 index 0000000000..a511ea4462 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt @@ -0,0 +1,3 @@ +data: {"id":"response-id","object":"text_completion","created":1646932609,"model":"ada","choices":[{"text":"Test chat streaming response","index":0,"logprobs":null,"finish_reason":"length"}],"usage":{"prompt_tokens":55,"completion_tokens":100,"total_tokens":155}} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json new file mode 100644 index 0000000000..5402294374 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json @@ -0,0 +1,19 @@ +{ + "id": "response-id", + "object": "text_completion", + "created": 1646932609, + "model": "ada", + "choices": [ + { + "text": "Test chat response", + "index": 0, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs new file mode 100644 index 0000000000..210845c219 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextEmbedding; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAITextEmbeddingGenerationServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAITextEmbeddingGenerationServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var service = includeLoggerFactory ? + new AzureOpenAITextEmbeddingGenerationService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextEmbeddingGenerationService("deployment", "https://endpoint", credentials, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new AzureOpenAITextEmbeddingGenerationService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextEmbeddingGenerationService("deployment", client, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task GenerateEmbeddingsForEmptyDataReturnsEmptyResultAsync() + { + // Arrange + var service = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + + // Act + var result = await service.GenerateEmbeddingsAsync([]); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task GenerateEmbeddingsWithEmptyResponseThrowsExceptionAsync() + { + // Arrange + var service = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(@"{ + ""object"": ""list"", + ""data"": [], + ""model"": ""model-id"" + }", Encoding.UTF8, "application/json") + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GenerateEmbeddingsAsync(["test"])); + Assert.Equal("Text embedding not found", exception.Message); + } + + [Fact] + public async Task GenerateEmbeddingsByDefaultWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(@"{ + ""object"": ""list"", + ""data"": [ + { + ""object"": ""embedding"", + ""embedding"": [ + 0.018990106880664825, + -0.0073809814639389515 + ], + ""index"": 0 + } + ], + ""model"": ""model-id"" + }", Encoding.UTF8, "application/json") + }; + + // Act + var result = await service.GenerateEmbeddingsAsync(["test"]); + + // Assert + Assert.Single(result); + + var memory = result[0]; + + Assert.Equal(0.018990106880664825, memory.Span[0]); + Assert.Equal(-0.0073809814639389515, memory.Span[1]); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs new file mode 100644 index 0000000000..bb9ff5fd42 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextEmbedding; + +/// +/// Unit tests for class. +/// +public sealed class OpenAITextEmbeddingGenerationServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAITextEmbeddingGenerationServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new OpenAITextEmbeddingGenerationService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : + new OpenAITextEmbeddingGenerationService("model-id", client); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task GenerateEmbeddingsForEmptyDataReturnsEmptyResultAsync() + { + // Arrange + var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient); + + // Act + var result = await service.GenerateEmbeddingsAsync([]); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task GenerateEmbeddingsWithEmptyResponseThrowsExceptionAsync() + { + // Arrange + var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(@"{ + ""object"": ""list"", + ""data"": [], + ""model"": ""model-id"" + }", Encoding.UTF8, "application/json") + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GenerateEmbeddingsAsync(["test"])); + Assert.Equal("Text embedding not found", exception.Message); + } + + [Fact] + public async Task GenerateEmbeddingsByDefaultWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(@"{ + ""object"": ""list"", + ""data"": [ + { + ""object"": ""embedding"", + ""embedding"": [ + 0.018990106880664825, + -0.0073809814639389515 + ], + ""index"": 0 + } + ], + ""model"": ""model-id"" + }", Encoding.UTF8, "application/json") + }; + + // Act + var result = await service.GenerateEmbeddingsAsync(["test"]); + + // Assert + Assert.Single(result); + + var memory = result[0]; + + Assert.Equal(0.018990106880664825, memory.Span[0]); + Assert.Equal(-0.0073809814639389515, memory.Span[1]); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs new file mode 100644 index 0000000000..87f5526d5f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextGeneration; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAITextGenerationServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAITextGenerationServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var service = includeLoggerFactory ? + new AzureOpenAITextGenerationService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextGenerationService("deployment", "https://endpoint", credentials, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new AzureOpenAITextGenerationService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextGenerationService("deployment", client, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task GetTextContentsWithEmptyChoicesThrowsExceptionAsync() + { + // Arrange + var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"id\":\"response-id\",\"object\":\"text_completion\",\"created\":1646932609,\"model\":\"ada\",\"choices\":[]}") + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GetTextContentsAsync("Prompt")); + + Assert.Equal("Text completions not found", exception.Message); + } + + [Theory] + [InlineData(0)] + [InlineData(129)] + public async Task GetTextContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) + { + // Arrange + var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new OpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GetTextContentsAsync("Prompt", settings)); + + Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task GetTextContentsHandlesSettingsCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new OpenAIPromptExecutionSettings + { + MaxTokens = 123, + Temperature = 0.6, + TopP = 0.5, + FrequencyPenalty = 1.6, + PresencePenalty = 1.2, + ResultsPerPrompt = 5, + TokenSelectionBiases = new Dictionary { { 2, 3 } }, + StopSequences = ["stop_sequence"] + }; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("text_completion_test_response.json")) + }; + + // Act + var result = await service.GetTextContentsAsync("Prompt", settings); + + // Assert + var requestContent = this._messageHandlerStub.RequestContent; + + Assert.NotNull(requestContent); + + var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); + + Assert.Equal("Prompt", content.GetProperty("prompt")[0].GetString()); + Assert.Equal(123, content.GetProperty("max_tokens").GetInt32()); + Assert.Equal(0.6, content.GetProperty("temperature").GetDouble()); + Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); + Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); + Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); + Assert.Equal(5, content.GetProperty("n").GetInt32()); + Assert.Equal(5, content.GetProperty("best_of").GetInt32()); + Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); + Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); + } + + [Fact] + public async Task GetTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("text_completion_test_response.json")) + }; + + // Act + var result = await service.GetTextContentsAsync("Prompt"); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Text); + + var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("text_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) + { + Assert.Equal("Test chat streaming response", chunk.Text); + } + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs new file mode 100644 index 0000000000..b8d804c21b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextGeneration; + +/// +/// Unit tests for class. +/// +public sealed class OpenAITextGenerationServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAITextGenerationServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAITextGenerationService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAITextGenerationService("model-id", "api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new OpenAITextGenerationService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : + new OpenAITextGenerationService("model-id", client); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task GetTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAITextGenerationService("model-id", "api-key", "organization", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("text_completion_test_response.json")) + }; + + // Act + var result = await service.GetTextContentsAsync("Prompt"); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Text); + + var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAITextGenerationService("model-id", "api-key", "organization", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("text_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) + { + Assert.Equal("Test chat streaming response", chunk.Text); + } + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs new file mode 100644 index 0000000000..a420a187d7 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToImage; + +/// +/// Unit tests for class. +/// +public sealed class OpenAITextToImageServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAITextToImageServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAITextToImageService("api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAITextToImageService("api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("organization", service.Attributes["Organization"]); + } + + [Theory] + [InlineData(123, 456, true)] + [InlineData(256, 512, true)] + [InlineData(256, 256, false)] + [InlineData(512, 512, false)] + [InlineData(1024, 1024, false)] + public async Task GenerateImageWorksCorrectlyAsync(int width, int height, bool expectedException) + { + // Arrange + var service = new OpenAITextToImageService("api-key", "organization", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(@"{ + ""created"": 1702575371, + ""data"": [ + { + ""url"": ""https://image-url"" + } + ] + }", Encoding.UTF8, "application/json") + }; + + // Act & Assert + if (expectedException) + { + await Assert.ThrowsAsync(() => service.GenerateImageAsync("description", width, height)); + } + else + { + var result = await service.GenerateImageAsync("description", width, height); + + Assert.Equal("https://image-url", result); + } + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs new file mode 100644 index 0000000000..ce04b69003 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using static Microsoft.SemanticKernel.Connectors.OpenAI.ToolCallBehavior; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI; + +/// +/// Unit tests for +/// +public sealed class ToolCallBehaviorTests +{ + [Fact] + public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() + { + // Arrange & Act + var behavior = ToolCallBehavior.EnableKernelFunctions; + + // Assert + Assert.IsType(behavior); + Assert.Equal(0, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() + { + // Arrange & Act + var behavior = ToolCallBehavior.AutoInvokeKernelFunctions; + + // Assert + Assert.IsType(behavior); + Assert.Equal(5, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void EnableFunctionsReturnsEnabledFunctionsInstance() + { + // Arrange & Act + List functions = [new("Plugin", "Function", "description", [], null)]; + var behavior = ToolCallBehavior.EnableFunctions(functions); + + // Assert + Assert.IsType(behavior); + } + + [Fact] + public void RequireFunctionReturnsRequiredFunctionInstance() + { + // Arrange & Act + var behavior = ToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); + + // Assert + Assert.IsType(behavior); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act + kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); + + // Assert + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + // Act + kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + var plugin = this.GetTestPlugin(); + + kernel.Plugins.Add(plugin); + + // Act + kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() + { + // Arrange + var enabledFunctions = new EnabledFunctions([], autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act + enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); + + // Assert + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null, chatCompletionsOptions)); + Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions)); + Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin_MyFunction is not available in the kernel.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool autoInvoke) + { + // Arrange + var plugin = this.GetTestPlugin(); + var functions = plugin.GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + kernel.Plugins.Add(plugin); + + // Act + enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); + } + + [Fact] + public void RequiredFunctionConfigureOptionsAddsTools() + { + // Arrange + var function = this.GetTestPlugin().GetFunctionsMetadata()[0].ToOpenAIFunction(); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + + // Act + requiredFunction.ConfigureOptions(null, chatCompletionsOptions); + + // Assert + Assert.NotNull(chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); + } + + private KernelPlugin GetTestPlugin() + { + var function = KernelFunctionFactory.CreateFromMethod( + (string parameter1, string parameter2) => "Result1", + "MyFunction", + "Test Function", + [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], + new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + } + + private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) + { + Assert.Single(chatCompletionsOptions.Tools); + + var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; + + Assert.NotNull(tool); + + Assert.Equal("MyPlugin_MyFunction", tool.Name); + Assert.Equal("Test Function", tool.Description); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.Parameters.ToString()); + } +} diff --git a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs index 133e7c76d9..ac19759e27 100644 --- a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs +++ b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.Experimental.Agents.Exceptions; using Microsoft.SemanticKernel.Experimental.Agents.Internal; +using Microsoft.SemanticKernel.Http; namespace Microsoft.SemanticKernel.Experimental.Agents; @@ -25,13 +26,9 @@ internal static partial class OpenAIRestExtensions request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}"); request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); - using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - throw new AgentException($"Unexpected failure: {response.StatusCode} [{url}]"); - } + using var response = await context.GetHttpClient().SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseBody = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); // Common case is for failure exception to be raised by REST invocation. // Null result is a logical possibility, but unlikely edge case. @@ -60,13 +57,10 @@ internal static partial class OpenAIRestExtensions request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}"); request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); - using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - throw new AgentException($"Unexpected failure: {response.StatusCode} [{url}]"); - } + using var response = await context.GetHttpClient().SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); + + var responseBody = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return JsonSerializer.Deserialize(responseBody) ?? throw new AgentException($"Null result processing: {typeof(TResult).Name}"); @@ -82,10 +76,6 @@ internal static partial class OpenAIRestExtensions request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}"); request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); - using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - throw new AgentException($"Unexpected failure: {response.StatusCode} [{url}]"); - } + using var response = await context.GetHttpClient().SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/HandlebarsPromptTemplateTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/HandlebarsPromptTemplateTests.cs index aba322d86e..80538e9aff 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/HandlebarsPromptTemplateTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/HandlebarsPromptTemplateTests.cs @@ -21,6 +21,20 @@ public sealed class HandlebarsPromptTemplateTests this._arguments = new() { ["input"] = Guid.NewGuid().ToString("X") }; } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ItInitializesHandlebarsPromptTemplateInstanceCorrectly(bool includeOptions) + { + // Arrange & Act + var template = includeOptions ? + new HandlebarsPromptTemplate(new()) : + new HandlebarsPromptTemplate(new(), new()); + + // Assert + Assert.NotNull(template); + } + [Fact] public async Task ItRendersVariablesAsync() { @@ -41,20 +55,14 @@ public sealed class HandlebarsPromptTemplateTests public async Task ItUsesDefaultValuesAsync() { // Arrange - var template = "Foo {{bar}} {{baz}}"; + var template = "Foo {{bar}} {{baz}}{{null}}{{empty}}"; var promptConfig = InitializeHbPromptConfig(template); - promptConfig.InputVariables.Add(new InputVariable() - { - Name = "bar", - Description = "Bar", - Default = "Bar" - }); - promptConfig.InputVariables.Add(new InputVariable() - { - Name = "baz", - Description = "Baz", - Default = "Baz" - }); + + promptConfig.InputVariables.Add(new() { Name = "bar", Description = "Bar", Default = "Bar" }); + promptConfig.InputVariables.Add(new() { Name = "baz", Description = "Baz", Default = "Baz" }); + promptConfig.InputVariables.Add(new() { Name = "null", Description = "Null", Default = null }); + promptConfig.InputVariables.Add(new() { Name = "empty", Description = "empty", Default = string.Empty }); + var target = (HandlebarsPromptTemplate)this._factory.Create(promptConfig); // Act diff --git a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelFunctionHelpersTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelFunctionHelpersTests.cs index cd026d0186..04e58b1d91 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelFunctionHelpersTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelFunctionHelpersTests.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Threading.Tasks; using HandlebarsDotNet; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.PromptTemplates.Handlebars; using Xunit; using static Extensions.UnitTests.PromptTemplates.Handlebars.TestUtilities; @@ -54,6 +55,30 @@ public sealed class KernelFunctionHelpersTests Assert.Equal("BazBar", result); } + [Fact] + public async Task ItThrowsExceptionWhenPositionalArgumentHasInvalidTypeAsync() + { + // Arrange + var template = "{{Foo-StringifyInt \"twelve\"}}"; + + // Act and Assert + var exception = await Assert.ThrowsAsync(() => this.RenderPromptTemplateAsync(template)); + + Assert.Contains("Invalid parameter type for function", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ItThrowsExceptionWhenPositionalArgumentNumberIsIncorrectAsync() + { + // Arrange + var template = "{{Foo-Combine \"Bar\"}}"; + + // Act and Assert + var exception = await Assert.ThrowsAsync(() => this.RenderPromptTemplateAsync(template)); + + Assert.Contains("Invalid parameter count for function", exception.Message, StringComparison.OrdinalIgnoreCase); + } + [Fact] public async Task ItRendersFunctionHelpersWitHashArgumentsAsync() { @@ -76,6 +101,17 @@ public sealed class KernelFunctionHelpersTests Assert.Matches("Parameter .* is required for function", exception.Message); } + [Fact] + public async Task ShouldThrowExceptionWhenArgumentsAreNotProvidedAsync() + { + // Arrange + var template = "{{Foo-Combine}}"; + + // Act and Assert + var exception = await Assert.ThrowsAsync(() => this.RenderPromptTemplateAsync(template)); + Assert.Matches("No arguments are provided for .*", exception.Message); + } + [Fact] public async Task ShouldThrowExceptionWhenFunctionHelperHasInvalidParameterTypeAsync() { @@ -98,6 +134,44 @@ public sealed class KernelFunctionHelpersTests Assert.Contains("Template references a helper that cannot be resolved", exception.Message, StringComparison.CurrentCultureIgnoreCase); } + [Fact] + public async Task ItCanReturnChatMessageContentAsync() + { + // Arrange + var template = "{{Foo-ChatMessageContent \"user\" \"User content\"}}"; + + // Act + var result = await this.RenderPromptTemplateAsync(template); + + // Assert + Assert.Equal("User content", result); + } + + [Theory] + [InlineData("{{Foo-RestApiOperationResponse \"text\" \"text/plain\"}}", "text")] + [InlineData("{{Foo-RestApiOperationResponse \'{\"key\":\"value\"}\' \'application/json\'}}", "[key, value]")] + public async Task ItCanReturnRestApiOperationResponseAsync(string template, string expectedResult) + { + // Arrange and Act + var result = await this.RenderPromptTemplateAsync(template); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task ItCanReturnCustomReturnTypeAsync() + { + // Arrange + var template = "{{Foo-CustomReturnType \"text\"}}"; + + // Act + var result = await this.RenderPromptTemplateAsync(template); + + // Assert + Assert.Equal("text", result); + } + private readonly HandlebarsPromptTemplateFactory _factory; private readonly Kernel _kernel; private readonly KernelArguments _arguments; @@ -132,5 +206,26 @@ public sealed class KernelFunctionHelpersTests [KernelFunction, Description("Return number as string")] public string StringifyInt([Description("Number to stringify")] int x) => x.ToString(CultureInfo.InvariantCulture); + + [KernelFunction, Description("Return ChatMessageContent")] + public ChatMessageContent ChatMessageContent(string role, string content) => new(new AuthorRole(role), content); + + [KernelFunction, Description("Return RestApiOperationResponse")] + public RestApiOperationResponse RestApiOperationResponse(string content, string contentType) => new(content, contentType); + + [KernelFunction, Description("Return CustomReturnType")] + public CustomReturnType CustomReturnType(string textProperty) => new(textProperty); + } + + private sealed class CustomReturnType + { + public CustomReturnType(string textProperty) + { + this.TextProperty = textProperty; + } + + public string TextProperty { get; set; } + + public override string ToString() => this.TextProperty; } } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelHelperUtilsTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelHelperUtilsTests.cs index a401785d57..e009cd61d4 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelHelperUtilsTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelHelperUtilsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Globalization; using HandlebarsDotNet; using Microsoft.SemanticKernel.PromptTemplates.Handlebars.Helpers; using Xunit; @@ -37,4 +38,41 @@ public class KernelHelperUtilsTests // Act & Assert Assert.Throws(() => KernelHelpersUtils.RegisterHelperSafe(handlebarsInstance, helperName, (HandlebarsReturnHelper)helper2)); } + + [Theory] + [InlineData(null, false)] + [InlineData(typeof(string), false)] + [InlineData(typeof(nuint), true)] + [InlineData(typeof(nint), true)] + [InlineData(typeof(sbyte), true)] + [InlineData(typeof(short), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(byte), true)] + [InlineData(typeof(ushort), true)] + [InlineData(typeof(uint), true)] + [InlineData(typeof(ulong), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(float), true)] + [InlineData(typeof(decimal), true)] + public void IsNumericTypeWorksCorrectly(Type? type, bool expectedResult) + { + Assert.Equal(expectedResult, KernelHelpersUtils.IsNumericType(type)); + } + + [Theory] + [MemberData(nameof(NumberInputs))] + public void TryParseAnyNumberWorksCorrectly(string number, bool expectedResult) + { + Assert.Equal(expectedResult, KernelHelpersUtils.TryParseAnyNumber(number)); + } + + public static TheoryData NumberInputs => new() + { + { 1234567890123456789L.ToString(CultureInfo.InvariantCulture), true }, + { 9876543210987654321UL.ToString(CultureInfo.InvariantCulture), true }, + { 123.456.ToString(CultureInfo.InvariantCulture), true }, + { 123456789.0123456789m.ToString(CultureInfo.InvariantCulture), true }, + { "test", false }, + }; } diff --git a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelSystemHelpersTests.cs b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelSystemHelpersTests.cs index 67af8abcae..c413e050cb 100644 --- a/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelSystemHelpersTests.cs +++ b/dotnet/src/Extensions/Extensions.UnitTests/PromptTemplates/Handlebars/Helpers/KernelSystemHelpersTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Text.Json.Nodes; using System.Threading.Tasks; +using HandlebarsDotNet; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.PromptTemplates.Handlebars; using Xunit; @@ -31,11 +33,12 @@ public sealed class KernelSystemHelpersTests Assert.Equal("Hello World!", result); } - [Fact] - public async Task ItRendersTemplateWithSetHelperAsync() + [Theory] + [InlineData("{{set name=\"x\" value=10}}{{json x}}")] + [InlineData("{{set \"x\" 10}}{{json x}}")] + public async Task ItRendersTemplateWithSetHelperAsync(string template) { // Arrange - var template = "{{set name=\"x\" value=10}}{{json x}}"; var arguments = new KernelArguments(); // Act @@ -45,14 +48,15 @@ public sealed class KernelSystemHelpersTests Assert.Equal("10", result); } - [Fact] - public async Task ItRendersTemplateWithJsonHelperAsync() + [Theory] + [MemberData(nameof(JsonObjectsToParse))] + public async Task ItRendersTemplateWithJsonHelperAsync(object json) { // Arrange var template = "{{json person}}"; var arguments = new KernelArguments { - { "person", new { name = "Alice", age = 25 } } + { "person", json } }; // Act @@ -62,6 +66,19 @@ public sealed class KernelSystemHelpersTests Assert.Equal("{\"name\":\"Alice\",\"age\":25}", result); } + [Fact] + public async Task ItThrowsExceptionWithJsonHelperWithoutArgumentsAsync() + { + // Arrange + var template = "{{json}}"; + + // Act + var exception = await Assert.ThrowsAsync(() => this.RenderPromptTemplateAsync(template)); + + // Assert + Assert.Equal("`json` helper requires a value to be passed in.", exception.Message); + } + [Fact] public async Task ComplexVariableTypeReturnsObjectAsync() { @@ -200,20 +217,65 @@ public sealed class KernelSystemHelpersTests Assert.Equal("Hello Alice!", result); } - [Fact] - public async Task ItRendersTemplateWithEqualHelperAsync() + [Theory] + [InlineData("{{or true true}}", "True")] + [InlineData("{{or true false}}", "True")] + [InlineData("{{or false false}}", "False")] + [InlineData("{{or x x}}", "True")] + [InlineData("{{or x y}}", "True")] + [InlineData("{{or x z}}", "True")] + [InlineData("{{or y y}}", "False")] + [InlineData("{{or y z}}", "False")] + [InlineData("{{or z z}}", "False")] + public async Task ItRendersTemplateWithOrHelperAsync(string template, string expectedResult) { // Arrange - var template = "{{#if (equals x y)}}Equal{{else}}Not equal{{/if}}"; - var arguments = new KernelArguments { { "x", 10 }, { "y", 10 } }; + var arguments = new KernelArguments { { "x", true }, { "y", false }, { "z", null } }; // Act var result = await this.RenderPromptTemplateAsync(template, arguments); // Assert - Assert.Equal("Equal", result); + Assert.Equal(expectedResult, result); } + [Theory] + [InlineData("{{#if (equals x y)}}Equal{{else}}Not equal{{/if}}", "Equal")] + [InlineData("{{#if (equals x)}}Equal{{else}}Not equal{{/if}}", "Not equal")] + [InlineData("{{#if (equals a b)}}Equal{{else}}Not equal{{/if}}", "Not equal")] + [InlineData("{{#if (equals b z)}}Equal{{else}}Not equal{{/if}}", "Equal")] + public async Task ItRendersTemplateWithEqualHelperAsync(string template, string expectedResult) + { + // Arrange + var arguments = new KernelArguments { { "x", 10 }, { "y", 10 }, { "a", null }, { "b", "test" }, { "z", "test" } }; + + // Act + var result = await this.RenderPromptTemplateAsync(template, arguments); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task ItThrowsExceptionIfMessageDoesNotContainRoleAsync() + { + // Arrange + var template = "{{#message attribute=\"value\"}}Hello World!{{/message}}"; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => this.RenderPromptTemplateAsync(template)); + + // Assert + Assert.Equal("Message must have a role.", exception.Message); + } + + public static TheoryData JsonObjectsToParse => new() + { + new { name = "Alice", age = 25 }, + "{\"name\":\"Alice\",\"age\":25}", + JsonNode.Parse("{\"name\":\"Alice\",\"age\":25}")! + }; + #region private private readonly HandlebarsPromptTemplateFactory _factory; diff --git a/dotnet/src/Extensions/PromptTemplates.Handlebars/Helpers/KernelHelpers/KernelSystemHelpers.cs b/dotnet/src/Extensions/PromptTemplates.Handlebars/Helpers/KernelHelpers/KernelSystemHelpers.cs index a0ddd1dbcb..6b562d17fb 100644 --- a/dotnet/src/Extensions/PromptTemplates.Handlebars/Helpers/KernelHelpers/KernelSystemHelpers.cs +++ b/dotnet/src/Extensions/PromptTemplates.Handlebars/Helpers/KernelHelpers/KernelSystemHelpers.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; using HandlebarsDotNet; using HandlebarsDotNet.Compiler; using static Microsoft.SemanticKernel.PromptTemplates.Handlebars.Helpers.KernelHelpersUtils; @@ -84,18 +83,19 @@ internal static class KernelSystemHelpers handlebarsInstance.RegisterHelper("json", (in HelperOptions options, in Context context, in Arguments arguments) => { - if (arguments.Length == 0 || arguments[0] is null) + if (arguments.Length == 0) { throw new HandlebarsRuntimeException("`json` helper requires a value to be passed in."); } var args = ProcessArguments(arguments, variables); object objectToSerialize = args[0]; - var type = objectToSerialize.GetType(); - return type == typeof(string) ? objectToSerialize - : type == typeof(JsonNode) ? objectToSerialize.ToString() - : JsonSerializer.Serialize(objectToSerialize); + return objectToSerialize switch + { + string stringObject => objectToSerialize, + _ => JsonSerializer.Serialize(objectToSerialize) + }; }); handlebarsInstance.RegisterHelper("concat", (in HelperOptions options, in Context context, in Arguments arguments) => @@ -130,7 +130,15 @@ internal static class KernelSystemHelpers handlebarsInstance.RegisterHelper("or", (in HelperOptions options, in Context context, in Arguments arguments) => { var args = ProcessArguments(arguments, variables); - return args.Any(arg => arg != null && arg is not false); + + return args.Any(arg => + { + return arg switch + { + bool booleanArg => booleanArg, + _ => arg is null + }; + }); }); handlebarsInstance.RegisterHelper("add", (in HelperOptions options, in Context context, in Arguments arguments) => diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationResponseTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationResponseTests.cs index 7724926754..9e96fa5991 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationResponseTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationResponseTests.cs @@ -101,4 +101,19 @@ public class RestApiOperationResponseTests //Assert Assert.False(result); } + + [Theory] + [InlineData(null, "")] + [InlineData("content", "content")] + public void ToStringReturnsString(object? content, string expectedContent) + { + // Arrange + var response = new RestApiOperationResponse(content!, "application/json"); + + // Act + var result = response.ToString(); + + // Assert + Assert.Equal(expectedContent, result); + } } diff --git a/dotnet/src/IntegrationTests/Fakes/ThrowingEmailPluginFake.cs b/dotnet/src/IntegrationTests/Fakes/ThrowingEmailPluginFake.cs index eb081c78c8..752e48b0b5 100644 --- a/dotnet/src/IntegrationTests/Fakes/ThrowingEmailPluginFake.cs +++ b/dotnet/src/IntegrationTests/Fakes/ThrowingEmailPluginFake.cs @@ -3,7 +3,6 @@ using System; using System.ComponentModel; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; #pragma warning disable CA1812 // Uninstantiated internal types diff --git a/dotnet/src/IntegrationTests/RedirectOutput.cs b/dotnet/src/IntegrationTests/RedirectOutput.cs index 1905b5849e..34cac5ba96 100644 --- a/dotnet/src/IntegrationTests/RedirectOutput.cs +++ b/dotnet/src/IntegrationTests/RedirectOutput.cs @@ -44,9 +44,16 @@ public class RedirectOutput : TextWriter, ILogger, ILoggerFactory public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - var message = formatter(state, exception); - this._output?.WriteLine(message); - this._logs.AppendLine(message); + try + { + var message = formatter(state, exception); + this._logs.AppendLine(message); + this._output?.WriteLine(message); + } + catch (InvalidOperationException ioe) + { + Console.WriteLine($"RedirectOutput failed, reason: {ioe}"); + } } public ILogger CreateLogger(string categoryName) => this; diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/CompilerServicesAttributes.cs b/dotnet/src/InternalUtilities/src/Diagnostics/CompilerServicesAttributes.cs index ee24ada7de..9ce82b36c0 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/CompilerServicesAttributes.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/CompilerServicesAttributes.cs @@ -4,11 +4,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; + namespace System.Runtime.CompilerServices; #if !NETCOREAPP [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +[ExcludeFromCodeCoverage] internal sealed class CallerArgumentExpressionAttribute : Attribute { public CallerArgumentExpressionAttribute(string parameterName) diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/ExceptionExtensions.cs b/dotnet/src/InternalUtilities/src/Diagnostics/ExceptionExtensions.cs index 10a9dad6c3..d23173bdec 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/ExceptionExtensions.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/ExceptionExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; using System.Threading; namespace System; @@ -7,6 +8,7 @@ namespace System; /// /// Exception extension methods. /// +[ExcludeFromCodeCoverage] internal static class ExceptionExtensions { /// diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/ExperimentalAttribute.cs b/dotnet/src/InternalUtilities/src/Diagnostics/ExperimentalAttribute.cs index 11b2df8c5e..1332155b0d 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/ExperimentalAttribute.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/ExperimentalAttribute.cs @@ -14,6 +14,7 @@ namespace System.Diagnostics.CodeAnalysis; /// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental /// feature is used. Authors can use this attribute to ship preview features in their assemblies. /// +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs b/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs index ce3d5e8be4..34f0de31ec 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/NullableAttributes.cs @@ -36,6 +36,7 @@ internal sealed class NotNullAttribute : Attribute } /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] internal sealed class MaybeNullWhenAttribute : Attribute { @@ -50,6 +51,7 @@ internal sealed class MaybeNullWhenAttribute : Attribute } /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] internal sealed class NotNullWhenAttribute : Attribute { @@ -64,6 +66,7 @@ internal sealed class NotNullWhenAttribute : Attribute } /// Specifies that the output will be non-null if the named parameter is non-null. +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] internal sealed class NotNullIfNotNullAttribute : Attribute { @@ -84,6 +87,7 @@ internal sealed class DoesNotReturnAttribute : Attribute } /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] internal sealed class DoesNotReturnIfAttribute : Attribute { @@ -103,6 +107,7 @@ internal sealed class DoesNotReturnIfAttribute : Attribute #if !NETCOREAPP || NETCOREAPP3_1 /// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] internal sealed class MemberNotNullAttribute : Attribute { @@ -124,6 +129,7 @@ internal sealed class MemberNotNullAttribute : Attribute } /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] internal sealed class MemberNotNullWhenAttribute : Attribute { diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs b/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs index 3f0198ed1f..c8e754c40c 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; namespace Microsoft.SemanticKernel; +[ExcludeFromCodeCoverage] internal static class Verify { private static readonly Regex s_asciiLettersDigitsUnderscoresRegex = new("^[0-9A-Za-z_]*$"); diff --git a/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs b/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs index 8c75851dc1..638a0026bb 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpClientExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; using System.Threading; @@ -8,6 +9,7 @@ using System.Threading.Tasks; namespace Microsoft.SemanticKernel.Http; +[ExcludeFromCodeCoverage] internal static class HttpClientExtensions { /// diff --git a/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs b/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs index 198f383cd0..d11b6dfa86 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +12,7 @@ namespace Microsoft.SemanticKernel.Http; /// /// Provides functionality for retrieving instances of HttpClient. /// +[ExcludeFromCodeCoverage] internal static class HttpClientProvider { /// diff --git a/dotnet/src/InternalUtilities/src/Http/HttpContentExtensions.cs b/dotnet/src/InternalUtilities/src/Http/HttpContentExtensions.cs index 95e851f8c4..917e8d3af6 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpContentExtensions.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpContentExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; using System.Threading.Tasks; @@ -9,6 +10,7 @@ namespace Microsoft.SemanticKernel.Http; /// /// Provides extension methods for working with HTTP content in a way that translates HttpRequestExceptions into HttpOperationExceptions. /// +[ExcludeFromCodeCoverage] internal static class HttpContentExtensions { /// diff --git a/dotnet/src/InternalUtilities/src/Http/HttpHeaderValues.cs b/dotnet/src/InternalUtilities/src/Http/HttpHeaderValues.cs index df52d8c0b3..9f918c6c0d 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpHeaderValues.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpHeaderValues.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.SemanticKernel.Http; /// Provides HTTP header values for common purposes. +[ExcludeFromCodeCoverage] internal static class HttpHeaderValues { /// User agent string to use for all HTTP requests issued by Semantic Kernel. diff --git a/dotnet/src/InternalUtilities/src/Http/HttpRequest.cs b/dotnet/src/InternalUtilities/src/Http/HttpRequest.cs index 883eb07d9d..d04afb58d5 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpRequest.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpRequest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -9,6 +10,7 @@ using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel; +[ExcludeFromCodeCoverage] internal static class HttpRequest { private static readonly HttpMethod s_patchMethod = new("PATCH"); diff --git a/dotnet/src/InternalUtilities/src/Linq/AsyncEnumerable.cs b/dotnet/src/InternalUtilities/src/Linq/AsyncEnumerable.cs index 19c6d44d1d..844ae7e2f5 100644 --- a/dotnet/src/InternalUtilities/src/Linq/AsyncEnumerable.cs +++ b/dotnet/src/InternalUtilities/src/Linq/AsyncEnumerable.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; @@ -8,6 +9,7 @@ using Microsoft.SemanticKernel; // Used for compatibility with System.Linq.Async Nuget pkg namespace System.Linq; +[ExcludeFromCodeCoverage] internal static class AsyncEnumerable { public static IAsyncEnumerable Empty() => EmptyAsyncEnumerable.Instance; diff --git a/dotnet/src/InternalUtilities/src/System/EnvExtensions.cs b/dotnet/src/InternalUtilities/src/System/EnvExtensions.cs index 960a01d64d..05cb235d71 100644 --- a/dotnet/src/InternalUtilities/src/System/EnvExtensions.cs +++ b/dotnet/src/InternalUtilities/src/System/EnvExtensions.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; + namespace System; +[ExcludeFromCodeCoverage] internal static class EnvExtensions { /// diff --git a/dotnet/src/InternalUtilities/src/System/InternalTypeConverter.cs b/dotnet/src/InternalUtilities/src/System/InternalTypeConverter.cs index 4bc817a2f6..bd92f686ab 100644 --- a/dotnet/src/InternalUtilities/src/System/InternalTypeConverter.cs +++ b/dotnet/src/InternalUtilities/src/System/InternalTypeConverter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Microsoft.SemanticKernel; @@ -10,6 +11,7 @@ namespace Microsoft.SemanticKernel; /// /// Provides internal utility methods for converting types to strings with consideration for CultureInfo. /// +[ExcludeFromCodeCoverage] internal static class InternalTypeConverter { /// diff --git a/dotnet/src/InternalUtilities/src/System/TypeConverterFactory.cs b/dotnet/src/InternalUtilities/src/System/TypeConverterFactory.cs index 3189d4cc3c..2049dda894 100644 --- a/dotnet/src/InternalUtilities/src/System/TypeConverterFactory.cs +++ b/dotnet/src/InternalUtilities/src/System/TypeConverterFactory.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Microsoft.SemanticKernel; @@ -9,6 +10,7 @@ namespace Microsoft.SemanticKernel; /// /// Factory for creating TypeConverter instances based on a provided type. /// +[ExcludeFromCodeCoverage] internal static class TypeConverterFactory { /// diff --git a/dotnet/src/InternalUtilities/src/Text/JsonOptionsCache.cs b/dotnet/src/InternalUtilities/src/Text/JsonOptionsCache.cs index f63ce6c0d9..c8278b4b06 100644 --- a/dotnet/src/InternalUtilities/src/Text/JsonOptionsCache.cs +++ b/dotnet/src/InternalUtilities/src/Text/JsonOptionsCache.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace Microsoft.SemanticKernel.Text; @@ -11,6 +12,7 @@ namespace Microsoft.SemanticKernel.Text; /// Once the System.Text.Json package is upgraded to 8.0+, this will no longer be /// necessary and the actual default can be used. /// +[ExcludeFromCodeCoverage] internal static class JsonOptionsCache { /// Singleton for . diff --git a/dotnet/src/InternalUtilities/src/Text/ReadOnlyMemoryConverter.cs b/dotnet/src/InternalUtilities/src/Text/ReadOnlyMemoryConverter.cs index ecce02508e..1a754aa552 100644 --- a/dotnet/src/InternalUtilities/src/Text/ReadOnlyMemoryConverter.cs +++ b/dotnet/src/InternalUtilities/src/Text/ReadOnlyMemoryConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization; @@ -12,6 +13,7 @@ namespace Microsoft.SemanticKernel.Text; // It should be removed once SK projects upgrade to System.Text.Json v8.0.0. /// Provides a converter for . +[ExcludeFromCodeCoverage] internal sealed class ReadOnlyMemoryConverter : JsonConverter> { /// An instance of a converter for float[] that all operations delegate to. diff --git a/dotnet/src/InternalUtilities/src/Type/TypeExtensions.cs b/dotnet/src/InternalUtilities/src/Type/TypeExtensions.cs index 8ca4be34dc..266ac9ac0b 100644 --- a/dotnet/src/InternalUtilities/src/Type/TypeExtensions.cs +++ b/dotnet/src/InternalUtilities/src/Type/TypeExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace System; @@ -8,6 +9,7 @@ namespace System; /// /// Extensions methods for . /// +[ExcludeFromCodeCoverage] internal static class TypeExtensions { /// diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Plugins.UnitTests.csproj b/dotnet/src/Plugins/Plugins.UnitTests/Plugins.UnitTests.csproj index 705bd9dce4..60d06162de 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Plugins.UnitTests.csproj +++ b/dotnet/src/Plugins/Plugins.UnitTests/Plugins.UnitTests.csproj @@ -22,6 +22,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ImageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ImageContentTests.cs new file mode 100644 index 0000000000..cac559777f --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ImageContentTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Contents; + +/// +/// Unit tests for class. +/// +public sealed class ImageContentTests +{ + [Fact] + public void ToStringReturnsString() + { + // Arrange + var content1 = new ImageContent(null!); + var content2 = new ImageContent(new Uri("https://endpoint/")); + + // Act + var result1 = content1.ToString(); + var result2 = content2.ToString(); + + // Assert + Assert.Empty(result1); + Assert.Equal("https://endpoint/", result2); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/TypeExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/TypeExtensionsTests.cs new file mode 100644 index 0000000000..7533af205d --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/TypeExtensionsTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using TypeExtensions = System.TypeExtensions; + +namespace SemanticKernel.UnitTests.Utilities; + +/// +/// Unit tests for class. +/// +public sealed class TypeExtensionsTests +{ + [Theory] + [InlineData(null, typeof(object), false)] + [InlineData(typeof(TestType), typeof(object), false)] + [InlineData(typeof(Task), typeof(TestType), true)] + [InlineData(typeof(TestType?), typeof(TestType), true)] + [InlineData(typeof(ValueTask), typeof(TestType), true)] + [InlineData(typeof(IEnumerable), typeof(List), true)] + [InlineData(typeof(IList), typeof(List), true)] + [InlineData(typeof(ICollection), typeof(List), true)] + [InlineData(typeof(IDictionary), typeof(Dictionary), true)] + public void TryGetGenericResultTypeWorksCorrectly(Type? type, Type expectedType, bool expectedResult) + { + // Arrange & Act + var result = type.TryGetGenericResultType(out var resultType); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(expectedType, resultType); + } + + private struct TestType { } +}