diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs index 14125e95b76..3d85937eab8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs @@ -203,7 +203,7 @@ public bool TryGetValue(string key, [NotNullWhen(true)] out T? value) /// Copies all of the entries from into the dictionary, overwriting any existing items in the dictionary with the same key. /// The items to add. - internal void SetAll(IEnumerable> items) + internal void SetAll(AdditionalPropertiesDictionary items) { _ = Throw.IfNull(items); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 0be912430fa..04afa990b4e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -168,14 +168,298 @@ public virtual ChatOptions Clone() if (StopSequences is not null) { - options.StopSequences = new List(StopSequences); + options.StopSequences = [.. StopSequences]; } if (Tools is not null) { - options.Tools = new List(Tools); + options.Tools = [.. Tools]; } return options; } + + /// Merges the options specified by into this instance. + /// The other options to be merged into this instance. + /// + /// If , properties on this instance will be overwritten by the values from ; + /// if , properties on this instance will only be updated if they are on this instance. + /// The default is . + /// + /// + /// Merging works by copying the values from into this instance. + /// + /// When is (the default): + /// + /// + /// For properties of primitive types (like or ), + /// properties on this instance will only be updated if they are . + /// + /// + /// For list types (like or ), + /// the elements of 's collection will be added into the corresponding collection + /// on this instance (no deduplication is performed). + /// + /// + /// For dictionary types (like ), a shallow copy is performed + /// on the entries from , adding them into the corresponding dictionary on this instance, + /// but only if the key does not already exist in this instance's dictionary. + /// + /// + /// + /// + /// When is , any non- properties on will + /// overwrite the corresponding properties on this instance. + /// + /// + /// For properties of primitive types (like or ), + /// properties on this instance will be updated with values from if they're non- + /// on . + /// + /// + /// For list types (like or ), + /// values from will replace any existing collections on this instance if the collection + /// is non- on + /// + /// + /// For dictionary types (like ), entries from + /// will be stored into this instance's dictionary, overwriting any entries with the same key in this instance's dictionary. + /// + /// + /// + /// + public virtual void Merge(ChatOptions? other, bool overwrite = false) + { + if (other is not null) + { + if (overwrite) + { + MergeOverwrite(other); + } + else + { + MergeDefault(other); + } + } + } + + private void MergeDefault(ChatOptions other) + { + if (other.AdditionalProperties is { Count: > 0 }) + { + if (AdditionalProperties is null) + { + AdditionalProperties = other.AdditionalProperties.Clone(); + } + else + { + foreach (var entry in other.AdditionalProperties) + { + _ = AdditionalProperties.TryAdd(entry.Key, entry.Value); + } + } + } + + AllowMultipleToolCalls ??= other.AllowMultipleToolCalls; + + ConversationId ??= other.ConversationId; + + FrequencyPenalty ??= other.FrequencyPenalty; + + Instructions ??= other.Instructions; + + MaxOutputTokens ??= other.MaxOutputTokens; + + ModelId ??= other.ModelId; + + PresencePenalty ??= other.PresencePenalty; + + if (other.RawRepresentationFactory is { } otherRrf) + { + RawRepresentationFactory = RawRepresentationFactory is { } originalRrf ? + client => originalRrf(client) ?? otherRrf(client) : + otherRrf; + } + + ResponseFormat ??= other.ResponseFormat; + + Seed ??= other.Seed; + + if (other.StopSequences is not null) + { + if (StopSequences is null) + { + StopSequences = [.. other.StopSequences]; + } + else if (StopSequences is List stopSequences) + { + stopSequences.AddRange(other.StopSequences); + } + else + { + foreach (var sequence in other.StopSequences) + { + StopSequences.Add(sequence); + } + } + } + + Temperature ??= other.Temperature; + + ToolMode ??= other.ToolMode; + + if (other.Tools is not null) + { + if (Tools is null) + { + Tools = [.. other.Tools]; + } + else if (Tools is List tools) + { + tools.AddRange(other.Tools); + } + else + { + foreach (var tool in other.Tools) + { + Tools.Add(tool); + } + } + } + + TopK ??= other.TopK; + + TopP ??= other.TopP; + } + + private void MergeOverwrite(ChatOptions other) + { + if (other.AdditionalProperties is { Count: > 0 }) + { + if (AdditionalProperties is null) + { + AdditionalProperties = other.AdditionalProperties.Clone(); + } + else + { + AdditionalProperties.SetAll(other.AdditionalProperties); + } + } + + if (other.AllowMultipleToolCalls is not null) + { + AllowMultipleToolCalls = other.AllowMultipleToolCalls; + } + + if (other.ConversationId is not null) + { + ConversationId = other.ConversationId; + } + + if (other.FrequencyPenalty is not null) + { + FrequencyPenalty = other.FrequencyPenalty; + } + + if (other.Instructions is not null) + { + Instructions = other.Instructions; + } + + if (other.MaxOutputTokens is not null) + { + MaxOutputTokens = other.MaxOutputTokens; + } + + if (other.ModelId is not null) + { + ModelId = other.ModelId; + } + + if (other.PresencePenalty is not null) + { + PresencePenalty = other.PresencePenalty; + } + + if (other.RawRepresentationFactory is not null) + { + RawRepresentationFactory = other.RawRepresentationFactory; + } + + if (other.ResponseFormat is not null) + { + ResponseFormat = other.ResponseFormat; + } + + if (other.Seed is not null) + { + Seed = other.Seed; + } + + if (other.StopSequences is not null) + { + if (StopSequences is null) + { + StopSequences = [.. other.StopSequences]; + } + else + { + StopSequences.Clear(); + if (StopSequences is List stopSequences) + { + stopSequences.AddRange(other.StopSequences); + } + else + { + foreach (var sequence in other.StopSequences) + { + StopSequences.Add(sequence); + } + } + } + } + + if (other.Temperature is not null) + { + Temperature = other.Temperature; + } + + if (other.ToolMode is not null) + { + ToolMode = other.ToolMode; + } + + if (other.Tools is not null) + { + if (Tools is null) + { + Tools = [.. other.Tools]; + } + else + { + Tools.Clear(); + if (Tools is List tools) + { + tools.AddRange(other.Tools); + } + else + { + foreach (var tool in other.Tools) + { + Tools.Add(tool); + } + } + } + } + + if (other.TopK is not null) + { + TopK = other.TopK; + } + + if (other.TopP is not null) + { + TopP = other.TopP; + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs index b9a13d43dd0..eafd903f269 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGenerationOptions.cs @@ -61,8 +61,122 @@ public int? Dimensions public virtual EmbeddingGenerationOptions Clone() => new() { - ModelId = ModelId, - Dimensions = Dimensions, AdditionalProperties = AdditionalProperties?.Clone(), + Dimensions = Dimensions, + ModelId = ModelId, + RawRepresentationFactory = RawRepresentationFactory, }; + + /// Merges the options specified by into this instance. + /// The other options to be merged into this instance. + /// + /// If , properties on this instance will be overwritten by the values from ; + /// if , properties on this instance will only be updated if they are on this instance. + /// The default is . + /// + /// + /// Merging works by copying the values from into this instance. + /// + /// When is (the default): + /// + /// + /// For properties of primitive types (like or ), + /// properties on this instance will only be updated if they are . + /// + /// + /// For dictionary types (like ), a shallow copy is performed + /// on the entries from , adding them into the corresponding dictionary on this instance, + /// but only if the key does not already exist in this instance's dictionary. + /// + /// + /// + /// + /// When is , any non- properties on will + /// overwrite the corresponding properties on this instance. + /// + /// + /// For properties of primitive types (like or ), + /// properties on this instance will be updated with values from if they're non- + /// on . + /// + /// + /// For dictionary types (like ), entries from + /// will be stored into this instance's dictionary, overwriting any entries with the same key in this instance's dictionary. + /// + /// + /// + /// + public virtual void Merge(EmbeddingGenerationOptions? other, bool overwrite = false) + { + if (other is not null) + { + if (overwrite) + { + MergeOverwrite(other); + } + else + { + MergeDefault(other); + } + } + } + + private void MergeDefault(EmbeddingGenerationOptions other) + { + if (other.AdditionalProperties is { Count: > 0 }) + { + if (AdditionalProperties is null) + { + AdditionalProperties = other.AdditionalProperties.Clone(); + } + else + { + foreach (var entry in other.AdditionalProperties) + { + _ = AdditionalProperties.TryAdd(entry.Key, entry.Value); + } + } + } + + Dimensions ??= other.Dimensions; + + ModelId ??= other.ModelId; + + if (other.RawRepresentationFactory is { } otherRrf) + { + RawRepresentationFactory = RawRepresentationFactory is { } originalRrf ? + generator => originalRrf(generator) ?? otherRrf(generator) : + otherRrf; + } + } + + private void MergeOverwrite(EmbeddingGenerationOptions other) + { + if (other.AdditionalProperties is { Count: > 0 }) + { + if (AdditionalProperties is null) + { + AdditionalProperties = other.AdditionalProperties.Clone(); + } + else + { + AdditionalProperties.SetAll(other.AdditionalProperties); + } + } + + if (other.Dimensions is not null) + { + Dimensions = other.Dimensions; + } + + if (other.ModelId is not null) + { + ModelId = other.ModelId; + } + + if (other.RawRepresentationFactory is not null) + { + RawRepresentationFactory = other.RawRepresentationFactory; + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 79776b0ecb4..30a54a3ff62 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -908,6 +908,10 @@ { "Member": "virtual Microsoft.Extensions.AI.ChatOptions Microsoft.Extensions.AI.ChatOptions.Clone();", "Stage": "Stable" + }, + { + "Member": "virtual void Microsoft.Extensions.AI.ChatOptions.Merge(Microsoft.Extensions.AI.ChatOptions? other, bool overwrite = false);", + "Stage": "Stable" } ], "Properties": [ @@ -1518,6 +1522,10 @@ { "Member": "virtual Microsoft.Extensions.AI.EmbeddingGenerationOptions Microsoft.Extensions.AI.EmbeddingGenerationOptions.Clone();", "Stage": "Stable" + }, + { + "Member": "virtual void Microsoft.Extensions.AI.EmbeddingGenerationOptions.Merge(Microsoft.Extensions.AI.EmbeddingGenerationOptions? other, bool overwrite = false);", + "Stage": "Stable" } ], "Properties": [ diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs index 5ff0135cec7..76c085432d6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs @@ -47,17 +47,141 @@ public class SpeechToTextOptions /// Produces a clone of the current instance. /// A clone of the current instance. - public virtual SpeechToTextOptions Clone() - { - SpeechToTextOptions options = new() + public virtual SpeechToTextOptions Clone() => + new() { + AdditionalProperties = AdditionalProperties?.Clone(), ModelId = ModelId, + RawRepresentationFactory = RawRepresentationFactory, SpeechLanguage = SpeechLanguage, - TextLanguage = TextLanguage, SpeechSampleRate = SpeechSampleRate, - AdditionalProperties = AdditionalProperties?.Clone(), + TextLanguage = TextLanguage, }; - return options; + /// Merges the options specified by into this instance. + /// The other options to be merged into this instance. + /// + /// If , properties on this instance will be overwritten by the values from ; + /// if , properties on this instance will only be updated if they are on this instance. + /// The default is . + /// + /// + /// Merging works by copying the values from into this instance. + /// + /// When is (the default): + /// + /// + /// For properties of primitive types (like or ), + /// properties on this instance will only be updated if they are . + /// + /// + /// For dictionary types (like ), a shallow copy is performed + /// on the entries from , adding them into the corresponding dictionary on this instance, + /// but only if the key does not already exist in this instance's dictionary. + /// + /// + /// + /// + /// When is , any non- properties on will + /// overwrite the corresponding properties on this instance. + /// + /// + /// For properties of primitive types (like or ), + /// properties on this instance will be updated with values from if they're non- + /// on . + /// + /// + /// For dictionary types (like ), entries from + /// will be stored into this instance's dictionary, overwriting any entries with the same key in this instance's dictionary. + /// + /// + /// + /// + public virtual void Merge(SpeechToTextOptions? other, bool overwrite = false) + { + if (other is not null) + { + if (overwrite) + { + MergeOverwrite(other); + } + else + { + MergeDefault(other); + } + } + } + + private void MergeDefault(SpeechToTextOptions other) + { + if (other.AdditionalProperties is { Count: > 0 }) + { + if (AdditionalProperties is null) + { + AdditionalProperties = other.AdditionalProperties.Clone(); + } + else + { + foreach (var entry in other.AdditionalProperties) + { + _ = AdditionalProperties.TryAdd(entry.Key, entry.Value); + } + } + } + + ModelId ??= other.ModelId; + + if (other.RawRepresentationFactory is { } otherRrf) + { + RawRepresentationFactory = RawRepresentationFactory is { } originalRrf ? + client => originalRrf(client) ?? otherRrf(client) : + otherRrf; + } + + SpeechLanguage ??= other.SpeechLanguage; + + SpeechSampleRate ??= other.SpeechSampleRate; + + TextLanguage ??= other.TextLanguage; + } + + private void MergeOverwrite(SpeechToTextOptions other) + { + if (other.AdditionalProperties is { Count: > 0 }) + { + if (AdditionalProperties is null) + { + AdditionalProperties = other.AdditionalProperties.Clone(); + } + else + { + AdditionalProperties.SetAll(other.AdditionalProperties); + } + } + + if (other.ModelId is not null) + { + ModelId = other.ModelId; + } + + if (other.RawRepresentationFactory is not null) + { + RawRepresentationFactory = other.RawRepresentationFactory; + } + + if (other.SpeechLanguage is not null) + { + SpeechLanguage = other.SpeechLanguage; + } + + if (other.SpeechSampleRate is not null) + { + SpeechSampleRate = other.SpeechSampleRate; + } + + if (other.TextLanguage is not null) + { + TextLanguage = other.TextLanguage; + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs index b7645c26245..0a343a0255d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs @@ -14,42 +14,29 @@ public class ChatOptionsTests public void Constructor_Parameterless_PropsDefaulted() { ChatOptions options = new(); + AssertDefaults(options); + AssertDefaults(options.Clone()); + } + + private static void AssertDefaults(ChatOptions options) + { + Assert.Null(options.AdditionalProperties); + Assert.Null(options.AllowMultipleToolCalls); Assert.Null(options.ConversationId); + Assert.Null(options.FrequencyPenalty); Assert.Null(options.Instructions); - Assert.Null(options.Temperature); Assert.Null(options.MaxOutputTokens); - Assert.Null(options.TopP); - Assert.Null(options.TopK); - Assert.Null(options.FrequencyPenalty); + Assert.Null(options.ModelId); Assert.Null(options.PresencePenalty); - Assert.Null(options.Seed); Assert.Null(options.ResponseFormat); - Assert.Null(options.ModelId); + Assert.Null(options.Temperature); + Assert.Null(options.RawRepresentationFactory); + Assert.Null(options.Seed); Assert.Null(options.StopSequences); - Assert.Null(options.AllowMultipleToolCalls); + Assert.Null(options.TopK); + Assert.Null(options.TopP); Assert.Null(options.ToolMode); Assert.Null(options.Tools); - Assert.Null(options.AdditionalProperties); - Assert.Null(options.RawRepresentationFactory); - - ChatOptions clone = options.Clone(); - Assert.Null(clone.ConversationId); - Assert.Null(clone.Instructions); - Assert.Null(clone.Temperature); - Assert.Null(clone.MaxOutputTokens); - Assert.Null(clone.TopP); - Assert.Null(clone.TopK); - Assert.Null(clone.FrequencyPenalty); - Assert.Null(clone.PresencePenalty); - Assert.Null(clone.Seed); - Assert.Null(clone.ResponseFormat); - Assert.Null(clone.ModelId); - Assert.Null(clone.StopSequences); - Assert.Null(clone.AllowMultipleToolCalls); - Assert.Null(clone.ToolMode); - Assert.Null(clone.Tools); - Assert.Null(clone.AdditionalProperties); - Assert.Null(clone.RawRepresentationFactory); } [Fact] @@ -131,6 +118,174 @@ public void Properties_Roundtrip() Assert.Equal(additionalProps, clone.AdditionalProperties); } + [Fact] + public void Merge_MembersCopiedOver_Default() + { + using TestChatClient cc1 = new(); + using TestChatClient cc2 = new(); + using TestChatClient cc3 = new(); + + ChatOptions options = new(); + AssertDefaults(options); + + options.Merge(null); + AssertDefaults(options); + + options.Merge(new ChatOptions()); + AssertDefaults(options); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "value" }, + AllowMultipleToolCalls = true, + ConversationId = "12345", + FrequencyPenalty = 0.1f, + Instructions = "Some instructions", + MaxOutputTokens = 10, + ModelId = "modelId", + PresencePenalty = 0.2f, + RawRepresentationFactory = c => c == cc1 ? new FormatException() : null, + ResponseFormat = ChatResponseFormat.Json, + Seed = 12345, + StopSequences = ["stop1", "stop2"], + Temperature = 0.3f, + ToolMode = ChatToolMode.RequireAny, + TopK = 5, + TopP = 0.4f, + Tools = [AIFunctionFactory.Create(() => 42), AIFunctionFactory.Create(() => 43)], + }); + + Assert.NotNull(options.AdditionalProperties); + Assert.Single(options.AdditionalProperties); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.IsType(options.RawRepresentationFactory?.Invoke(cc1)); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc3)); + + Assert.True(options.AllowMultipleToolCalls); + Assert.Equal("12345", options.ConversationId); + Assert.Equal(0.1f, options.FrequencyPenalty); + Assert.Equal("Some instructions", options.Instructions); + Assert.Equal(10, options.MaxOutputTokens); + Assert.Equal("modelId", options.ModelId); + Assert.Equal(0.2f, options.PresencePenalty); + Assert.NotNull(options.RawRepresentationFactory); + Assert.Same(ChatResponseFormat.Json, options.ResponseFormat); + Assert.Equal(12345, options.Seed); + Assert.Equal(["stop1", "stop2"], options.StopSequences); + Assert.Equal(0.3f, options.Temperature); + Assert.Same(ChatToolMode.RequireAny, options.ToolMode); + Assert.Equal(5, options.TopK); + Assert.Equal(0.4f, options.TopP); + Assert.NotNull(options.Tools); + Assert.Equal(2, options.Tools.Count); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "changedvalue", ["key2"] = "value2" }, + Instructions = "Updated instructions", + MaxOutputTokens = 42, + RawRepresentationFactory = c => c == cc2 ? new ArgumentException() : null, + Tools = [AIFunctionFactory.Create(() => 44)], + }); + + Assert.Equal("Some instructions", options.Instructions); + Assert.Equal(10, options.MaxOutputTokens); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(2, options.AdditionalProperties.Count); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.NotNull(options.Tools); + Assert.Equal(3, options.Tools.Count); + Assert.IsType(options.RawRepresentationFactory?.Invoke(cc1)); + Assert.IsType(options.RawRepresentationFactory?.Invoke(cc2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc3)); + } + + [Fact] + public void Merge_MembersCopiedOver_Overwrite() + { + using TestChatClient cc1 = new(); + using TestChatClient cc2 = new(); + using TestChatClient cc3 = new(); + + ChatOptions options = new(); + AssertDefaults(options); + + options.Merge(null, overwrite: true); + AssertDefaults(options); + + options.Merge(new ChatOptions(), overwrite: true); + AssertDefaults(options); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "value" }, + AllowMultipleToolCalls = true, + ConversationId = "12345", + FrequencyPenalty = 0.1f, + Instructions = "Some instructions", + MaxOutputTokens = 10, + ModelId = "modelId", + PresencePenalty = 0.2f, + RawRepresentationFactory = c => c == cc1 ? new FormatException() : null, + ResponseFormat = ChatResponseFormat.Json, + Seed = 12345, + StopSequences = ["stop1", "stop2"], + Temperature = 0.3f, + ToolMode = ChatToolMode.RequireAny, + TopK = 5, + TopP = 0.4f, + Tools = [AIFunctionFactory.Create(() => 42), AIFunctionFactory.Create(() => 43)], + }, overwrite: true); + + Assert.NotNull(options.AdditionalProperties); + Assert.Single(options.AdditionalProperties); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.IsType(options.RawRepresentationFactory?.Invoke(cc1)); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc3)); + + Assert.True(options.AllowMultipleToolCalls); + Assert.Equal("12345", options.ConversationId); + Assert.Equal(0.1f, options.FrequencyPenalty); + Assert.Equal("Some instructions", options.Instructions); + Assert.Equal(10, options.MaxOutputTokens); + Assert.Equal("modelId", options.ModelId); + Assert.Equal(0.2f, options.PresencePenalty); + Assert.NotNull(options.RawRepresentationFactory); + Assert.Same(ChatResponseFormat.Json, options.ResponseFormat); + Assert.Equal(12345, options.Seed); + Assert.Equal(["stop1", "stop2"], options.StopSequences); + Assert.Equal(0.3f, options.Temperature); + Assert.Same(ChatToolMode.RequireAny, options.ToolMode); + Assert.Equal(5, options.TopK); + Assert.Equal(0.4f, options.TopP); + Assert.NotNull(options.Tools); + Assert.Equal(2, options.Tools.Count); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "changedvalue", ["key2"] = "value2" }, + Instructions = "Updated instructions", + MaxOutputTokens = 42, + RawRepresentationFactory = c => c == cc2 ? new ArgumentException() : null, + Tools = [AIFunctionFactory.Create(() => 44)], + }, overwrite: true); + + Assert.Equal("Updated instructions", options.Instructions); + Assert.Equal(42, options.MaxOutputTokens); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(2, options.AdditionalProperties.Count); + Assert.Equal("changedvalue", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.NotNull(options.Tools); + Assert.Single(options.Tools); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc1)); + Assert.IsType(options.RawRepresentationFactory?.Invoke(cc2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(cc3)); + } + [Fact] public void JsonSerialization_Roundtrips() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 8b13d640ae1..1a54c001313 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -77,7 +77,7 @@ public static IEnumerable ToChatResponse_Coalescing_VariousSequenceAnd { foreach (bool gapBeginningEnd in new[] { false, true }) { - yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false }; + yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, gapBeginningEnd }; } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs index 97ffecfc1f6..d5b4e4e4263 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Embeddings/EmbeddingGenerationOptionsTests.cs @@ -13,14 +13,16 @@ public class EmbeddingGenerationOptionsTests public void Constructor_Parameterless_PropsDefaulted() { EmbeddingGenerationOptions options = new(); - Assert.Null(options.ModelId); + AssertDefaults(options); + AssertDefaults(options.Clone()); + } + + private static void AssertDefaults(EmbeddingGenerationOptions options) + { Assert.Null(options.AdditionalProperties); Assert.Null(options.Dimensions); - - EmbeddingGenerationOptions clone = options.Clone(); - Assert.Null(clone.ModelId); - Assert.Null(clone.AdditionalProperties); - Assert.Null(clone.Dimensions); + Assert.Null(options.ModelId); + Assert.Null(options.RawRepresentationFactory); } [Fact] @@ -31,6 +33,114 @@ public void InvalidArgs_Throws() Assert.Throws("value", () => options.Dimensions = -1); } + [Fact] + public void Merge_MembersCopiedOver_Default() + { + using TestEmbeddingGenerator g1 = new(); + using TestEmbeddingGenerator g2 = new(); + using TestEmbeddingGenerator g3 = new(); + + EmbeddingGenerationOptions options = new(); + AssertDefaults(options); + + options.Merge(null); + AssertDefaults(options); + + options.Merge(new EmbeddingGenerationOptions()); + AssertDefaults(options); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "value", ["key2"] = "value2" }, + Dimensions = 1536, + ModelId = "modelId", + RawRepresentationFactory = c => c == g1 ? new FormatException() : null, + }); + + Assert.Equal(1536, options.Dimensions); + Assert.Equal("modelId", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(2, options.AdditionalProperties.Count); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.IsType(options.RawRepresentationFactory?.Invoke(g1)); + Assert.Null(options.RawRepresentationFactory?.Invoke(g2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(g3)); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "changedvalue", ["key3"] = "value3" }, + Dimensions = 386, + ModelId = "modelId2", + RawRepresentationFactory = c => c == g2 ? new ArgumentException() : null, + }); + + Assert.Equal(1536, options.Dimensions); + Assert.Equal("modelId", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(3, options.AdditionalProperties.Count); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.IsType(options.RawRepresentationFactory?.Invoke(g1)); + Assert.IsType(options.RawRepresentationFactory?.Invoke(g2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(g3)); + } + + [Fact] + public void Merge_MembersCopiedOver_Overwrite() + { + using TestEmbeddingGenerator g1 = new(); + using TestEmbeddingGenerator g2 = new(); + using TestEmbeddingGenerator g3 = new(); + + EmbeddingGenerationOptions options = new(); + AssertDefaults(options); + + options.Merge(null, overwrite: true); + AssertDefaults(options); + + options.Merge(new EmbeddingGenerationOptions(), overwrite: true); + AssertDefaults(options); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "value", ["key2"] = "value2" }, + Dimensions = 1536, + ModelId = "modelId", + RawRepresentationFactory = c => c == g1 ? new FormatException() : null, + }, overwrite: true); + + Assert.Equal(1536, options.Dimensions); + Assert.Equal("modelId", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(2, options.AdditionalProperties.Count); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.IsType(options.RawRepresentationFactory?.Invoke(g1)); + Assert.Null(options.RawRepresentationFactory?.Invoke(g2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(g3)); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "changedvalue", ["key3"] = "value3" }, + Dimensions = 386, + ModelId = "modelId2", + RawRepresentationFactory = c => c == g2 ? new ArgumentException() : null, + }, overwrite: true); + + Assert.Equal(386, options.Dimensions); + Assert.Equal("modelId2", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(3, options.AdditionalProperties.Count); + Assert.Equal("changedvalue", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.Equal("value3", options.AdditionalProperties["key3"]); + Assert.Null(options.RawRepresentationFactory?.Invoke(g1)); + Assert.IsType(options.RawRepresentationFactory?.Invoke(g2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(g3)); + } + [Fact] public void Properties_Roundtrip() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs index 20936fd4517..9dd00459f9a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextOptionsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Text.Json; using Xunit; @@ -12,16 +13,139 @@ public class SpeechToTextOptionsTests public void Constructor_Parameterless_PropsDefaulted() { SpeechToTextOptions options = new(); + AssertDefaults(options); + AssertDefaults(options.Clone()); + } + + private static void AssertDefaults(SpeechToTextOptions options) + { + Assert.Null(options.AdditionalProperties); Assert.Null(options.ModelId); + Assert.Null(options.RawRepresentationFactory); Assert.Null(options.SpeechLanguage); Assert.Null(options.SpeechSampleRate); - Assert.Null(options.AdditionalProperties); + } - SpeechToTextOptions clone = options.Clone(); - Assert.Null(clone.ModelId); - Assert.Null(clone.SpeechLanguage); - Assert.Null(clone.SpeechSampleRate); - Assert.Null(clone.AdditionalProperties); + [Fact] + public void Merge_MembersCopiedOver_Default() + { + using TestSpeechToTextClient c1 = new(); + using TestSpeechToTextClient c2 = new(); + using TestSpeechToTextClient c3 = new(); + + SpeechToTextOptions options = new(); + AssertDefaults(options); + + options.Merge(null); + AssertDefaults(options); + + options.Merge(new SpeechToTextOptions()); + AssertDefaults(options); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "value" }, + SpeechLanguage = "en-US", + SpeechSampleRate = 44100, + TextLanguage = "fr-FR", + ModelId = "modelId", + RawRepresentationFactory = c => c == c1 ? new FormatException() : null, + }); + + Assert.Equal("en-US", options.SpeechLanguage); + Assert.Equal("fr-FR", options.TextLanguage); + Assert.Equal(44100, options.SpeechSampleRate); + Assert.Equal("modelId", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Single(options.AdditionalProperties); + Assert.True(options.AdditionalProperties.ContainsKey("key")); + Assert.IsType(options.RawRepresentationFactory?.Invoke(c1)); + Assert.Null(options.RawRepresentationFactory?.Invoke(c2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(c3)); + + options.Merge(new() + { + AdditionalProperties = new() { ["key2"] = "value2" }, + SpeechLanguage = "fr-FR", + SpeechSampleRate = 12345, + TextLanguage = "de-DE", + ModelId = "modelId2", + RawRepresentationFactory = c => c == c2 ? new ArgumentException() : null, + }); + + Assert.Equal("en-US", options.SpeechLanguage); + Assert.Equal("fr-FR", options.TextLanguage); + Assert.Equal(44100, options.SpeechSampleRate); + Assert.Equal("modelId", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(2, options.AdditionalProperties.Count); + Assert.True(options.AdditionalProperties.ContainsKey("key")); + Assert.True(options.AdditionalProperties.ContainsKey("key2")); + Assert.IsType(options.RawRepresentationFactory?.Invoke(c1)); + Assert.IsType(options.RawRepresentationFactory?.Invoke(c2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(c3)); + } + + [Fact] + public void Merge_MembersCopiedOver_Overwrite() + { + using TestSpeechToTextClient c1 = new(); + using TestSpeechToTextClient c2 = new(); + using TestSpeechToTextClient c3 = new(); + + SpeechToTextOptions options = new(); + AssertDefaults(options); + + options.Merge(null, overwrite: true); + AssertDefaults(options); + + options.Merge(new SpeechToTextOptions(), overwrite: true); + AssertDefaults(options); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "value", ["key2"] = "value2" }, + SpeechLanguage = "en-US", + SpeechSampleRate = 44100, + TextLanguage = "fr-FR", + ModelId = "modelId", + RawRepresentationFactory = c => c == c1 ? new FormatException() : null, + }, overwrite: true); + + Assert.Equal("en-US", options.SpeechLanguage); + Assert.Equal("fr-FR", options.TextLanguage); + Assert.Equal(44100, options.SpeechSampleRate); + Assert.Equal("modelId", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(2, options.AdditionalProperties.Count); + Assert.Equal("value", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.IsType(options.RawRepresentationFactory?.Invoke(c1)); + Assert.Null(options.RawRepresentationFactory?.Invoke(c2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(c3)); + + options.Merge(new() + { + AdditionalProperties = new() { ["key"] = "changedvalue", ["key3"] = "value3" }, + SpeechLanguage = "fr-FR", + SpeechSampleRate = 12345, + TextLanguage = "de-DE", + ModelId = "modelId2", + RawRepresentationFactory = c => c == c2 ? new ArgumentException() : null, + }, overwrite: true); + + Assert.Equal("fr-FR", options.SpeechLanguage); + Assert.Equal("de-DE", options.TextLanguage); + Assert.Equal(12345, options.SpeechSampleRate); + Assert.Equal("modelId2", options.ModelId); + Assert.NotNull(options.AdditionalProperties); + Assert.Equal(3, options.AdditionalProperties.Count); + Assert.Equal("changedvalue", options.AdditionalProperties["key"]); + Assert.Equal("value2", options.AdditionalProperties["key2"]); + Assert.Equal("value3", options.AdditionalProperties["key3"]); + Assert.Null(options.RawRepresentationFactory?.Invoke(c1)); + Assert.IsType(options.RawRepresentationFactory?.Invoke(c2)); + Assert.Null(options.RawRepresentationFactory?.Invoke(c3)); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs index f0a2f08ab13..03936076d17 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/SpeechToText/SpeechToTextResponseUpdateExtensionsTests.cs @@ -24,7 +24,7 @@ public static IEnumerable ToSpeechToTextResponse_Coalescing_VariousSeq { foreach (bool gapBeginningEnd in new[] { false, true }) { - yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false }; + yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, gapBeginningEnd }; } } }