mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
.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:  ### 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:
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
-1
@@ -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]);
|
||||
}
|
||||
}
|
||||
+70
@@ -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"]);
|
||||
}
|
||||
}
|
||||
+59
@@ -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" } }] } };
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -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)
|
||||
{ }
|
||||
}
|
||||
+81
@@ -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);
|
||||
}
|
||||
}
|
||||
+74
@@ -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"]);
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
}
|
||||
+583
@@ -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 { }
|
||||
}
|
||||
+78
-2
@@ -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()
|
||||
{
|
||||
+113
-5
@@ -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();
|
||||
|
||||
+45
@@ -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()
|
||||
{
|
||||
|
||||
+66
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -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);
|
||||
|
||||
+497
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
+56
@@ -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
|
||||
}
|
||||
}
|
||||
+32
@@ -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
|
||||
}
|
||||
}
|
||||
+9
@@ -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]
|
||||
+3
@@ -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]
|
||||
+3
@@ -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]
|
||||
+22
@@ -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
|
||||
}
|
||||
+1
@@ -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}]}]}
|
||||
+28
@@ -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
|
||||
}
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"id": "32ba9f77-d620-4b6c-9265-ad50cb314a5c",
|
||||
"status": "notRunning"
|
||||
}
|
||||
-12
@@ -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"
|
||||
}
|
||||
+3
@@ -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]
|
||||
+19
@@ -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
|
||||
}
|
||||
}
|
||||
+152
@@ -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();
|
||||
}
|
||||
}
|
||||
+135
@@ -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();
|
||||
}
|
||||
}
|
||||
+208
@@ -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();
|
||||
}
|
||||
}
|
||||
+113
@@ -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();
|
||||
}
|
||||
}
|
||||
+85
@@ -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
-13
@@ -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
|
||||
|
||||
+95
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
+73
-11
@@ -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;
|
||||
|
||||
+15
-7
@@ -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 { }
|
||||
}
|
||||
Reference in New Issue
Block a user