.Net: Increase code coverage and add threshold to build pipeline (#4610)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Resolves: https://github.com/microsoft/semantic-kernel/issues/4372

Added more tests and increased code coverage for production packages to
meet 80% threshold. Added new steps to PR build pipeline to check code
coverage for production packages and trigger a failure if code coverage
in PR is lower than defined threshold.

Example of code coverage output:

![image](https://github.com/microsoft/semantic-kernel/assets/13853051/2a271dcf-5e87-4006-a3b1-5bcfce829f3a)

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
This commit is contained in:
Dmytro Struk
2024-01-17 12:04:30 +00:00
committed by GitHub
parent 4ec03bedaf
commit 4205cd3a79
68 changed files with 3557 additions and 85 deletions
+54
View File
@@ -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
}
+18 -1
View File
@@ -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/[email protected]
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()
+6
View File
@@ -118,6 +118,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10
src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs
EndProjectSection
EndProject
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}") = "Plugins.Core", "src\Plugins\Plugins.Core\Plugins.Core.csproj", "{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCalcPlugins", "samples\NCalcPlugins\NCalcPlugins.csproj", "{E6EDAB8F-3406-4DBF-9AAB-DF40DC2CA0FA}"
@@ -500,6 +505,7 @@ 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}
+60
View File
@@ -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
@@ -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);
}
/// <summary>
/// Logs Azure OpenAI action details.
/// </summary>
/// <param name="callerMemberName">Caller member name. Populated automatically by runtime.</param>
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);
}
}
}
@@ -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);
@@ -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);
}
}
@@ -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<TextToImageResponse.Image, string> 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
{
@@ -27,14 +27,17 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Numerics.Tensors" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/AssertExtensions.cs"
Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/AssertExtensions.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
@@ -55,16 +58,10 @@
</ItemGroup>
<ItemGroup>
<None Update="HuggingFace\TestData\completion_test_response.json">
<None Update="HuggingFace\TestData\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="HuggingFace\TestData\embeddings_test_response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="OpenAI\TestData\image_result_test_response.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="OpenAI\TestData\image_generation_test_response.json">
<None Update="OpenAI\TestData\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
@@ -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<HttpRequestHeaders?> RequestHeaders { get; private set; }
public List<HttpContentHeaders?> ContentHeaders { get; private set; }
public List<byte[]?> RequestContents { get; private set; }
public List<Uri?> RequestUris { get; private set; }
public List<HttpMethod?> Methods { get; private set; }
public List<HttpResponseMessage> ResponsesToReturn { get; set; }
public MultipleHttpMessageHandlerStub()
{
this.RequestHeaders = [];
this.ContentHeaders = [];
this.RequestContents = [];
this.RequestUris = [];
this.Methods = [];
this.ResponsesToReturn = [];
}
protected override async Task<HttpResponseMessage> 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]);
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="AzureOpenAIWithDataChatMessageContent"/> class.
/// </summary>
public sealed class AzureOpenAIWithDataChatMessageContentTests
{
[Fact]
public void ConstructorThrowsExceptionWhenAssistantMessageIsNotProvided()
{
// Arrange
var choice = new ChatWithDataChoice();
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => 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<string, object?>()) :
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"]);
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="AzureOpenAIWithDataStreamingChatMessageContent"/> class.
/// </summary>
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<object[]> 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<object[]> 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" } }] } };
}
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIChatMessageContent"/> class.
/// </summary>
public sealed class OpenAIChatMessageContentTests
{
[Fact]
public void ConstructorsWorkCorrectly()
{
// Arrange
List<ChatCompletionsToolCall> 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<ChatCompletionsToolCall> 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<string, object?> { { "key", "value" } };
List<ChatCompletionsToolCall> 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<List<ChatCompletionsFunctionToolCall>>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]);
var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List<ChatCompletionsFunctionToolCall>;
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<ChatCompletionsToolCall> 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)
{ }
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIFunctionToolCall"/> class.
/// </summary>
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<int, string>();
var functionNamesByIndex = new Dictionary<int, string>();
var functionArgumentBuildersByIndex = new Dictionary<int, StringBuilder>();
// Act
var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls(
ref toolCallIdsByIndex,
ref functionNamesByIndex,
ref functionArgumentBuildersByIndex);
// Assert
Assert.Empty(toolCalls);
}
[Fact]
public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls()
{
// Arrange
var toolCallIdsByIndex = new Dictionary<int, string> { { 3, "test-id" } };
var functionNamesByIndex = new Dictionary<int, string> { { 3, "test-function" } };
var functionArgumentBuildersByIndex = new Dictionary<int, StringBuilder> { { 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);
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIPluginCollectionExtensions"/> class.
/// </summary>
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"]);
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIStreamingTextContent"/> class.
/// </summary>
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);
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="RequestFailedExceptionExtensions"/> class.
/// </summary>
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<HttpOperationException>(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<HttpOperationException>(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<HttpHeader> _headers = new List<HttpHeader>();
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<HttpHeader> 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<string>? values) => throw new NotImplementedException();
#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes).
}
#endregion
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="AzureOpenAIChatCompletionService"/>
/// </summary>
public sealed class AzureOpenAIChatCompletionServiceTests : IDisposable
{
private readonly MultipleHttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
public AzureOpenAIChatCompletionServiceTests()
{
this._messageHandlerStub = new MultipleHttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(true);
this._mockLoggerFactory.Setup(l => l.CreateLogger(It.IsAny<string>())).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<KernelException>(() => 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<ArgumentOutOfRangeException>(() => 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<int, int> { { 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<JsonElement>(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<JsonElement>(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<HttpResponseMessage>();
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<JsonElement>(firstContent);
var secondContentJson = JsonSerializer.Deserialize<JsonElement>(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<HttpResponseMessage>();
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<JsonElement>(firstContent);
var secondContentJson = JsonSerializer.Deserialize<JsonElement>(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<ToolCallBehavior> ToolCallBehaviors => new()
{
ToolCallBehavior.EnableKernelFunctions,
ToolCallBehavior.AutoInvokeKernelFunctions
};
public static TheoryData<object, string?> ResponseFormats => new()
{
{ new FakeChatCompletionsResponseFormat(), null },
{ "json_object", "json_object" },
{ "text", "text" }
};
private sealed class FakeChatCompletionsResponseFormat : ChatCompletionsResponseFormat { }
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIChatCompletionService"/>
/// </summary>
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<ILoggerFactory> _mockLoggerFactory;
public OpenAIChatCompletionTests()
public OpenAIChatCompletionServiceTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
IList<KernelFunctionMetadata> 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()
{
@@ -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<ILoggerFactory> _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<ILoggerFactory>();
}
[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();
@@ -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()
{
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIMemoryBuilderExtensions"/> class.
/// </summary>
public sealed class OpenAIMemoryBuilderExtensionsTests
{
private readonly Mock<IMemoryStore> _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);
}
}
@@ -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);
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAIServiceCollectionExtensions"/> class.
/// </summary>
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<OpenAIClient>(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<ITextGenerationService>();
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<OpenAIClient>(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<ITextGenerationService>();
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<OpenAIClient>(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<ITextGenerationService>();
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<OpenAIClient>(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<ITextGenerationService>();
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<OpenAIClient>(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<ITextEmbeddingGenerationService>();
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<OpenAIClient>(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<ITextEmbeddingGenerationService>();
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<OpenAIClient>(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<ITextEmbeddingGenerationService>();
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<OpenAIClient>(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<ITextEmbeddingGenerationService>();
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<OpenAIClient>(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<IChatCompletionService>();
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<OpenAIClient>(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<IChatCompletionService>();
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<OpenAIClient>(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<IChatCompletionService>();
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<OpenAIClient>(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<IChatCompletionService>();
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<ITextToImageService>();
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<ITextToImageService>();
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<ITextToImageService>();
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<ITextToImageService>();
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"
};
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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]
@@ -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]
@@ -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]
@@ -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
}
@@ -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}]}]}
@@ -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
}
}
@@ -1,4 +0,0 @@
{
"id": "32ba9f77-d620-4b6c-9265-ad50cb314a5c",
"status": "notRunning"
}
@@ -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"
}
@@ -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]
@@ -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
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="AzureOpenAITextEmbeddingGenerationService"/> class.
/// </summary>
public sealed class AzureOpenAITextEmbeddingGenerationServiceTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
public AzureOpenAITextEmbeddingGenerationServiceTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
}
[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<KernelException>(() => 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();
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAITextEmbeddingGenerationService"/> class.
/// </summary>
public sealed class OpenAITextEmbeddingGenerationServiceTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
public OpenAITextEmbeddingGenerationServiceTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
}
[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<KernelException>(() => 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();
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="AzureOpenAITextGenerationService"/> class.
/// </summary>
public sealed class AzureOpenAITextGenerationServiceTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
public AzureOpenAITextGenerationServiceTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
}
[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<KernelException>(() => 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<ArgumentOutOfRangeException>(() => 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<int, int> { { 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<JsonElement>(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();
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAITextGenerationService"/> class.
/// </summary>
public sealed class OpenAITextGenerationServiceTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
public OpenAITextGenerationServiceTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
}
[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();
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="OpenAITextToImageService"/> class.
/// </summary>
public sealed class OpenAITextToImageServiceTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
public OpenAITextToImageServiceTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
this._mockLoggerFactory = new Mock<ILoggerFactory>();
}
[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<ArgumentOutOfRangeException>(() => 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();
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="ToolCallBehavior"/>
/// </summary>
public sealed class ToolCallBehaviorTests
{
[Fact]
public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance()
{
// Arrange & Act
var behavior = ToolCallBehavior.EnableKernelFunctions;
// Assert
Assert.IsType<KernelFunctions>(behavior);
Assert.Equal(0, behavior.MaximumAutoInvokeAttempts);
}
[Fact]
public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance()
{
// Arrange & Act
var behavior = ToolCallBehavior.AutoInvokeKernelFunctions;
// Assert
Assert.IsType<KernelFunctions>(behavior);
Assert.Equal(5, behavior.MaximumAutoInvokeAttempts);
}
[Fact]
public void EnableFunctionsReturnsEnabledFunctionsInstance()
{
// Arrange & Act
List<OpenAIFunction> functions = [new("Plugin", "Function", "description", [], null)];
var behavior = ToolCallBehavior.EnableFunctions(functions);
// Assert
Assert.IsType<EnabledFunctions>(behavior);
}
[Fact]
public void RequireFunctionReturnsRequiredFunctionInstance()
{
// Arrange & Act
var behavior = ToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null));
// Assert
Assert.IsType<RequiredFunction>(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<KernelException>(() => 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<KernelException>(() => 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());
}
}
@@ -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
@@ -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<KernelException>(() => 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<KernelException>(() => 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<ArgumentException>(() => 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;
}
}
@@ -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<InvalidOperationException>(() => 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<string, bool> 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 },
};
}
@@ -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("<title~>Hello World!</title~>", 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<HandlebarsRuntimeException>(() => 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<KernelException>(() => this.RenderPromptTemplateAsync(template));
// Assert
Assert.Equal("Message must have a role.", exception.Message);
}
public static TheoryData<object> JsonObjectsToParse => new()
{
new { name = "Alice", age = 25 },
"{\"name\":\"Alice\",\"age\":25}",
JsonNode.Parse("{\"name\":\"Alice\",\"age\":25}")!
};
#region private
private readonly HandlebarsPromptTemplateFactory _factory;
@@ -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) =>
@@ -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);
}
}
@@ -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)
@@ -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;
/// <summary>
/// Exception extension methods.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class ExceptionExtensions
{
/// <summary>
@@ -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.
/// </remarks>
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.Assembly |
AttributeTargets.Module |
AttributeTargets.Class |
@@ -36,6 +36,7 @@ internal sealed class NotNullAttribute : Attribute
}
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
@@ -50,6 +51,7 @@ internal sealed class MaybeNullWhenAttribute : Attribute
}
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
@@ -64,6 +66,7 @@ internal sealed class NotNullWhenAttribute : Attribute
}
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[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
}
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
@@ -103,6 +107,7 @@ internal sealed class DoesNotReturnIfAttribute : Attribute
#if !NETCOREAPP || NETCOREAPP3_1
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary>
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute
{
@@ -124,6 +129,7 @@ internal sealed class MemberNotNullAttribute : Attribute
}
/// <summary>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.</summary>
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
@@ -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_]*$");
@@ -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
{
/// <summary>
@@ -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;
/// <summary>
/// Provides functionality for retrieving instances of HttpClient.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class HttpClientProvider
{
/// <summary>
@@ -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;
/// <summary>
/// Provides extension methods for working with HTTP content in a way that translates HttpRequestExceptions into HttpOperationExceptions.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class HttpContentExtensions
{
/// <summary>
@@ -1,8 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel.Http;
/// <summary>Provides HTTP header values for common purposes.</summary>
[ExcludeFromCodeCoverage]
internal static class HttpHeaderValues
{
/// <summary>User agent string to use for all HTTP requests issued by Semantic Kernel.</summary>
@@ -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");
@@ -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<T> Empty<T>() => EmptyAsyncEnumerable<T>.Instance;
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace System;
[ExcludeFromCodeCoverage]
internal static class EnvExtensions
{
/// <summary>
@@ -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;
/// <summary>
/// Provides internal utility methods for converting types to strings with consideration for CultureInfo.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class InternalTypeConverter
{
/// <summary>
@@ -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;
/// <summary>
/// Factory for creating TypeConverter instances based on a provided type.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class TypeConverterFactory
{
/// <summary>
@@ -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.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static class JsonOptionsCache
{
/// <summary>Singleton for <see cref="ReadOnlyMemoryConverter"/>.</summary>
@@ -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.
/// <summary>Provides a converter for <see cref="ReadOnlyMemory{Single}"/>.</summary>
[ExcludeFromCodeCoverage]
internal sealed class ReadOnlyMemoryConverter : JsonConverter<ReadOnlyMemory<float>>
{
/// <summary>An instance of a converter for float[] that all operations delegate to.</summary>
@@ -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;
/// <summary>
/// Extensions methods for <see cref="System.Type"/>.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class TypeExtensions
{
/// <summary>
@@ -22,6 +22,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
</ItemGroup>
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.SemanticKernel;
using Xunit;
namespace SemanticKernel.UnitTests.Contents;
/// <summary>
/// Unit tests for <see cref="ImageContent"/> class.
/// </summary>
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);
}
}
@@ -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;
/// <summary>
/// Unit tests for <see cref="TypeExtensions"/> class.
/// </summary>
public sealed class TypeExtensionsTests
{
[Theory]
[InlineData(null, typeof(object), false)]
[InlineData(typeof(TestType), typeof(object), false)]
[InlineData(typeof(Task<TestType>), typeof(TestType), true)]
[InlineData(typeof(TestType?), typeof(TestType), true)]
[InlineData(typeof(ValueTask<TestType>), typeof(TestType), true)]
[InlineData(typeof(IEnumerable<TestType>), typeof(List<TestType>), true)]
[InlineData(typeof(IList<TestType>), typeof(List<TestType>), true)]
[InlineData(typeof(ICollection<TestType>), typeof(List<TestType>), true)]
[InlineData(typeof(IDictionary<string, TestType>), typeof(Dictionary<string, TestType>), 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 { }
}