diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs index 397c9bb0e39d..43ae9a2b76a1 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -1936,6 +1936,16 @@ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext co { "{\"voice\":\"echo\",\"format\":\"opus\"}", "{\"voice\":\"echo\",\"format\":\"opus\"}" }, }; + public static TheoryData AudioMimeTypeMappingData => new() + { + { ChatOutputAudioFormat.Wav, "audio/wav" }, + { ChatOutputAudioFormat.Aac, "audio/aac" }, + { ChatOutputAudioFormat.Mp3, "audio/mp3" }, + { ChatOutputAudioFormat.Opus, "audio/opus" }, + { ChatOutputAudioFormat.Flac, "audio/flac" }, + { ChatOutputAudioFormat.Pcm16, "audio/pcm16" }, + }; + #pragma warning disable CS8618, CA1812 private sealed class MathReasoning { @@ -2146,6 +2156,56 @@ public async Task ItHandlesAudioContentWithMetadataInResponseAsync() // The ExpiresAt value is converted to a DateTime object, so we can't directly compare it to the Unix timestamp } + [Theory] + [MemberData(nameof(AudioMimeTypeMappingData))] + public async Task ItMapsAudioOutputFormatToCorrectMimeTypeAsync(ChatOutputAudioFormat format, string expectedMimeType) + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-4o", apiKey: "NOKEY", httpClient: this._httpClient); + + var responseJson = """ + { + "model": "gpt-4o", + "choices": [ + { + "message": { + "role": "assistant", + "content": "This is the text response.", + "audio": { + "data": "AQIDBA==" + } + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 20, + "total_tokens": 30 + } + } + """; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { Content = new StringContent(responseJson) }; + + var settings = new OpenAIPromptExecutionSettings + { + Modalities = ChatResponseModalities.Text | ChatResponseModalities.Audio, + Audio = new ChatAudioOptions(ChatOutputAudioVoice.Alloy, format) + }; + + // Act + var result = await chatCompletion.GetChatMessageContentAsync(this._chatHistoryForTest, settings); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Items.Count); + var audioContent = result.Items[1] as AudioContent; + Assert.NotNull(audioContent); + Assert.Equal(expectedMimeType, audioContent.MimeType); + } + [Fact] public async Task GetChatMessageContentsThrowsExceptionWithEmptyBinaryContentAsync() { diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 7f8dfa41a2d9..8e96bb8912b5 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -999,9 +999,9 @@ private OpenAIChatMessageContent CreateChatMessageContent(OAIChat.ChatCompletion return "audio/opus"; } - if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Wav) + if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Aac) { - return "audio/wav"; + return "audio/aac"; } if (audioOptions.OutputAudioFormat == ChatOutputAudioFormat.Flac) @@ -1014,7 +1014,7 @@ private OpenAIChatMessageContent CreateChatMessageContent(OAIChat.ChatCompletion return "audio/pcm16"; } - throw new NotSupportedException($"Unsupported audio output format '{audioOptions.OutputAudioFormat}'. Supported formats are 'wav', 'mp3', 'opus', 'flac' and 'pcm16'."); + throw new NotSupportedException($"Unsupported audio output format '{audioOptions.OutputAudioFormat}'. Supported formats are 'wav', 'mp3', 'opus', 'aac', 'flac' and 'pcm16'."); } private OpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs index d35c29ad6a78..80901b5340ce 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.SemanticKernel.Process.Internal; using Microsoft.SemanticKernel.Process.Models; @@ -50,5 +51,9 @@ public KernelProcess(KernelProcessState state, IList step Verify.NotNullOrWhiteSpace(state.Name); this.Steps = [.. steps]; + if (threads is not null) + { + this.Threads = threads.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } } } diff --git a/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs b/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs new file mode 100644 index 000000000000..11bc7431683c --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/KernelProcessTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests; + +/// +/// Unit tests for . +/// +public class KernelProcessTests +{ + /// + /// Verifies that the constructor assigns the supplied + /// threads dictionary to the property + /// instead of silently discarding it. + /// + [Fact] + public void Constructor_AssignsThreadsParameter_WhenProvided() + { + // Arrange + var state = new KernelProcessState(name: "TestProcess", version: "v1", id: "p1"); + var steps = new List(); + var threads = new Dictionary + { + ["main"] = new KernelProcessAgentThread { ThreadName = "main", ThreadId = "t-1" }, + ["aux"] = new KernelProcessAgentThread { ThreadName = "aux", ThreadId = "t-2" }, + }; + + // Act + var process = new KernelProcess(state, steps, edges: null, threads: threads); + + // Assert + Assert.Equal(2, process.Threads.Count); + Assert.True(process.Threads.ContainsKey("main")); + Assert.True(process.Threads.ContainsKey("aux")); + Assert.Equal("t-1", process.Threads["main"].ThreadId); + Assert.Equal("t-2", process.Threads["aux"].ThreadId); + } + + /// + /// Verifies that the constructor leaves + /// as an empty dictionary when the + /// threads argument is null. + /// + [Fact] + public void Constructor_DefaultsThreadsToEmptyDictionary_WhenNull() + { + // Arrange + var state = new KernelProcessState(name: "TestProcess", version: "v1", id: "p2"); + var steps = new List(); + + // Act + var process = new KernelProcess(state, steps, edges: null, threads: null); + + // Assert + Assert.NotNull(process.Threads); + Assert.Empty(process.Threads); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs index d7976ad2dbd0..3c2accd5942b 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettingsExtensions.cs @@ -179,15 +179,17 @@ public static class PromptExecutionSettingsExtensions options.ToolMode = ChatToolMode.Auto; options.AllowMultipleToolCalls = autoChoiceBehavior.Options?.AllowParallelCalls; } - else if (settings.FunctionChoiceBehavior is NoneFunctionChoiceBehavior noneFunctionChoiceBehavior) - { - options.ToolMode = ChatToolMode.None; - } - else if (settings.FunctionChoiceBehavior is RequiredFunctionChoiceBehavior requiredFunctionChoiceBehavior) - { - options.ToolMode = ChatToolMode.RequireAny; - options.AllowMultipleToolCalls = requiredFunctionChoiceBehavior.Options?.AllowParallelCalls; - } + else + if (settings.FunctionChoiceBehavior is NoneFunctionChoiceBehavior noneFunctionChoiceBehavior) + { + options.ToolMode = ChatToolMode.None; + } + else + if (settings.FunctionChoiceBehavior is RequiredFunctionChoiceBehavior requiredFunctionChoiceBehavior) + { + options.ToolMode = ChatToolMode.RequireAny; + options.AllowMultipleToolCalls = requiredFunctionChoiceBehavior.Options?.AllowParallelCalls; + } options.Tools = []; foreach (var function in functions) diff --git a/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs b/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs index d58ba26c6555..25e7cd9a8766 100644 --- a/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs +++ b/dotnet/src/SemanticKernel.Core/Data/TextSearchStore/TextSearchStore.cs @@ -202,12 +202,7 @@ public async Task> GetTextSearchResultsAsy var searchResult = await this.SearchInternalAsync(query, searchOptions, cancellationToken).ConfigureAwait(false); var results = searchResult.Select(x => new TextSearchResult(x.Text ?? string.Empty) { Name = x.SourceName, Link = x.SourceLink }); - return new(searchResult.Select(x => - new TextSearchResult(x.Text ?? string.Empty) - { - Name = x.SourceName, - Link = x.SourceLink - }).ToAsyncEnumerable()); + return new(results.ToAsyncEnumerable()); } /// diff --git a/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs b/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs index a2a39a35ea7b..49543725399c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Data/TextSearchStoreTests.cs @@ -245,6 +245,37 @@ public async Task SearchAsyncReturnsSearchResults() Assert.Equal("Sample text", actualResultsList[0]); } + [Fact] + public async Task GetTextSearchResultsAsyncReturnsTextSearchResults() + { + // Arrange + var mockResults = new List.TextRagStorageDocument>> + { + new(new TextSearchStore.TextRagStorageDocument + { + Text = "Sample text", + SourceName = "src-name", + SourceLink = "src-link", + }, 0.9f) + }; + + this._recordCollectionMock + .Setup(r => r.SearchAsync("query", 3, It.IsAny.TextRagStorageDocument>>(), It.IsAny())) + .Returns(mockResults.ToAsyncEnumerable()); + + using var store = new TextSearchStore(this._vectorStoreMock.Object, "testCollection", 128); + + // Act + var actualResults = await store.GetTextSearchResultsAsync("query"); + + // Assert + var actualResultsList = await actualResults.Results.ToListAsync(); + Assert.Single(actualResultsList); + Assert.Equal("Sample text", actualResultsList[0].Value); + Assert.Equal("src-name", actualResultsList[0].Name); + Assert.Equal("src-link", actualResultsList[0].Link); + } + [Fact] public async Task SearchAsyncWithHybridReturnsSearchResults() {