diff --git a/dotnet/src/SemanticKernel.IntegrationTests/AI/AIServiceType.cs b/dotnet/src/SemanticKernel.IntegrationTests/AI/AIServiceType.cs new file mode 100644 index 000000000000..901903634e02 --- /dev/null +++ b/dotnet/src/SemanticKernel.IntegrationTests/AI/AIServiceType.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.AI; + +/// +/// Enumeration to run integration tests for different AI services +/// +public enum AIServiceType +{ + /// + /// Open AI service + /// + OpenAI = 0, + + /// + /// Azure Open AI service + /// + AzureOpenAI = 1 +} diff --git a/dotnet/src/SemanticKernel.IntegrationTests/AI/OpenAICompletionTests.cs b/dotnet/src/SemanticKernel.IntegrationTests/AI/OpenAICompletionTests.cs index 3b9c186c472a..63351caec1f9 100644 --- a/dotnet/src/SemanticKernel.IntegrationTests/AI/OpenAICompletionTests.cs +++ b/dotnet/src/SemanticKernel.IntegrationTests/AI/OpenAICompletionTests.cs @@ -31,6 +31,9 @@ public OpenAICompletionTests(ITestOutputHelper output) .AddEnvironmentVariables() .AddUserSecrets() .Build(); + + this._serviceConfiguration.Add(AIServiceType.OpenAI, this.ConfigureOpenAI); + this._serviceConfiguration.Add(AIServiceType.AzureOpenAI, this.ConfigureAzureOpenAI); } [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] @@ -40,15 +43,7 @@ public async Task OpenAITestAsync(string prompt, string expectedAnswerContains) // Arrange IKernel target = Kernel.Builder.WithLogger(this._logger).Build(); - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - target.Config.AddOpenAITextCompletion( - serviceId: openAIConfiguration.Label, - modelId: openAIConfiguration.ModelId, - apiKey: openAIConfiguration.ApiKey); - - target.Config.SetDefaultTextCompletionService(openAIConfiguration.Label); + this.ConfigureOpenAI(target); IDictionary skill = TestHelpers.GetSkill("ChatSkill", target); @@ -66,18 +61,7 @@ public async Task AzureOpenAITestAsync(string prompt, string expectedAnswerConta // Arrange IKernel target = Kernel.Builder.WithLogger(this._logger).Build(); - // OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - // Assert.NotNull(openAIConfiguration); - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - target.Config.AddAzureOpenAITextCompletion( - serviceId: azureOpenAIConfiguration.Label, - deploymentName: azureOpenAIConfiguration.DeploymentName, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey); - - target.Config.SetDefaultTextCompletionService(azureOpenAIConfiguration.Label); + this.ConfigureAzureOpenAI(target); IDictionary skill = TestHelpers.GetSkill("ChatSkill", target); @@ -118,11 +102,41 @@ public async Task OpenAIHttpRetryPolicyTestAsync(string prompt, string expectedO Assert.Contains(expectedOutput, this._testOutputHelper.GetLogs(), StringComparison.InvariantCultureIgnoreCase); } + [Theory(Skip = "This test is for manual verification.")] + [InlineData("\n", AIServiceType.OpenAI)] + [InlineData("\r\n", AIServiceType.OpenAI)] + [InlineData("\n", AIServiceType.AzureOpenAI)] + [InlineData("\r\n", AIServiceType.AzureOpenAI)] + public async Task CompletionWithDifferentLineEndingsAsync(string lineEnding, AIServiceType service) + { + // Arrange + var prompt = + $"Given a json input and a request. Apply the request on the json input and return the result. " + + $"Put the result in between tags{lineEnding}" + + $"Input:{lineEnding}{{\"name\": \"John\", \"age\": 30}}{lineEnding}{lineEnding}Request:{lineEnding}name"; + + const string expectedAnswerContains = "John"; + + IKernel target = Kernel.Builder.WithLogger(this._logger).Build(); + + this._serviceConfiguration[service](target); + + IDictionary skill = TestHelpers.GetSkill("ChatSkill", target); + + // Act + SKContext actual = await target.RunAsync(prompt, skill["Chat"]); + + // Assert + Assert.Contains(expectedAnswerContains, actual.Result, StringComparison.InvariantCultureIgnoreCase); + } + #region internals private readonly XunitLogger _logger; private readonly RedirectOutput _testOutputHelper; + private readonly Dictionary> _serviceConfiguration = new(); + public void Dispose() { this.Dispose(true); @@ -143,5 +157,34 @@ private void Dispose(bool disposing) } } + private void ConfigureOpenAI(IKernel kernel) + { + var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + + Assert.NotNull(openAIConfiguration); + + kernel.Config.AddOpenAITextCompletion( + serviceId: openAIConfiguration.Label, + modelId: openAIConfiguration.ModelId, + apiKey: openAIConfiguration.ApiKey); + + kernel.Config.SetDefaultTextCompletionService(openAIConfiguration.Label); + } + + private void ConfigureAzureOpenAI(IKernel kernel) + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + + Assert.NotNull(azureOpenAIConfiguration); + + kernel.Config.AddAzureOpenAITextCompletion( + serviceId: azureOpenAIConfiguration.Label, + deploymentName: azureOpenAIConfiguration.DeploymentName, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey); + + kernel.Config.SetDefaultTextCompletionService(azureOpenAIConfiguration.Label); + } + #endregion } diff --git a/dotnet/src/SemanticKernel.UnitTests/Text/StringExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Text/StringExtensionsTests.cs new file mode 100644 index 000000000000..c5b7a1cd5889 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Text/StringExtensionsTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Text; +using Xunit; + +namespace SemanticKernel.UnitTests.Text; + +/// +/// Unit tests for +/// +public class StringExtensionsTests +{ + [Theory] + [InlineData("\r\n", "\n")] + [InlineData("Test string\r\n", "Test string\n")] + [InlineData("\r\nTest string", "\nTest string")] + [InlineData("\r\nTest string\r\n", "\nTest string\n")] + public void ItNormalizesLineEndingsCorrectly(string input, string expectedString) + { + // Act + input = input.NormalizeLineEndings(); + + // Assert + Assert.Equal(expectedString, input); + } +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs index ab6431328e0a..ad3d31614649 100644 --- a/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs @@ -74,7 +74,7 @@ public async Task CompleteAsync(string text, CompleteRequestSettings req var requestBody = Json.Serialize(new AzureTextCompletionRequest { - Prompt = text, + Prompt = text.NormalizeLineEndings(), Temperature = requestSettings.Temperature, TopP = requestSettings.TopP, PresencePenalty = requestSettings.PresencePenalty, diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs index 5194f1d6227b..96b7ac1f8052 100644 --- a/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs @@ -74,7 +74,7 @@ public Task CompleteAsync(string text, CompleteRequestSettings requestSe var requestBody = Json.Serialize(new OpenAITextCompletionRequest { Model = this._modelId, - Prompt = text, + Prompt = text.NormalizeLineEndings(), Temperature = requestSettings.Temperature, TopP = requestSettings.TopP, PresencePenalty = requestSettings.PresencePenalty, diff --git a/dotnet/src/SemanticKernel/Text/StringExtensions.cs b/dotnet/src/SemanticKernel/Text/StringExtensions.cs index fcf60d771127..f94636ff8d27 100644 --- a/dotnet/src/SemanticKernel/Text/StringExtensions.cs +++ b/dotnet/src/SemanticKernel/Text/StringExtensions.cs @@ -12,4 +12,9 @@ internal static bool EqualsIgnoreCase(this string src, string value) { return src.Equals(value, StringComparison.InvariantCultureIgnoreCase); } + + internal static string NormalizeLineEndings(this string src) + { + return src.Replace("\r\n", "\n", StringComparison.InvariantCultureIgnoreCase); + } }