diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props index 7c5ac8424e0..a68b0e4298f 100644 --- a/eng/MSBuild/Shared.props +++ b/eng/MSBuild/Shared.props @@ -1,8 +1,4 @@ - - - - diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs index 28b513cda4a..616ad284198 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs @@ -4,6 +4,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; namespace Microsoft.Extensions.AI; @@ -45,7 +47,7 @@ public AdditionalPropertiesDictionary(IEnumerable> /// A shallow clone of the properties dictionary. The instance will not be the same as the current instance, /// but it will contain all of the same key-value pairs. /// - public AdditionalPropertiesDictionary Clone() => new AdditionalPropertiesDictionary(_dictionary); + public AdditionalPropertiesDictionary Clone() => new(_dictionary); /// public object? this[string key] @@ -94,6 +96,9 @@ public object? this[string key] /// public IEnumerator> GetEnumerator() => _dictionary.GetEnumerator(); + /// + IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator(); + /// public bool Remove(string key) => _dictionary.Remove(key); @@ -103,6 +108,52 @@ public object? this[string key] /// public bool TryGetValue(string key, out object? value) => _dictionary.TryGetValue(key, out value); - /// - IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator(); + /// Attempts to extract a typed value from the dictionary. + /// Specifies the type of the value to be retrieved. + /// The key to locate. + /// + /// The value retrieved from the dictionary, if found and successfully converted to the requested type; + /// otherwise, the default value of . + /// + /// + /// if a non- value was found for + /// in the dictionary and converted to the requested type; otherwise, . + /// + /// + /// If a non- is found for the key in the dictionary, but the value is not of the requested type but is + /// an object, the method will attempt to convert the object to the requested type. + /// + public bool TryGetValue(string key, [NotNullWhen(true)] out T? value) + { + if (TryGetValue(key, out object? obj)) + { + switch (obj) + { + case T t: + // The object is already of the requested type. Return it. + value = t; + return true; + + case IConvertible: + // The object is convertible; try to convert it to the requested type. Unfortunately, there's no + // convenient way to do this that avoids exceptions and that doesn't involve a ton of boilerplate, + // so we only try when the source object is at least an IConvertible, which is what ChangeType uses. + try + { + value = (T)Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); + return true; + } + catch (Exception e) when (e is ArgumentException or FormatException or InvalidCastException or OverflowException) + { + // Ignore known failure modes. + } + + break; + } + } + + // Unable to find the value or convert it to the requested type. + value = default; + return false; + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index 4aa2ab89d73..2906a24e0ce 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -19,7 +19,6 @@ - true true true true diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj index d1f802ace8a..622495618c6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj @@ -20,7 +20,6 @@ true - true true true true diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj b/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj index ac0abe33c10..0a562ead7d0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj @@ -21,7 +21,6 @@ true true - true true true true diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs index 61827d45cc9..6aee8978ac4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs @@ -11,7 +11,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; #pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?) @@ -298,7 +297,7 @@ private OllamaChatRequest ToOllamaChatRequest(IList chatMessages, C void TransferMetadataValue(string propertyName, Action setOption) { - if (options.AdditionalProperties?.TryGetConvertedValue(propertyName, out T? t) is true) + if (options.AdditionalProperties?.TryGetValue(propertyName, out T? t) is true) { request.Options ??= new(); setOption(request.Options, t); diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs index b0ecf08895c..6a34a2ff811 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaEmbeddingGenerator.cs @@ -8,7 +8,6 @@ using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -75,12 +74,12 @@ public async Task>> GenerateAsync(IEnumerab if (options?.AdditionalProperties is { } requestProps) { - if (requestProps.TryGetConvertedValue("keep_alive", out long keepAlive)) + if (requestProps.TryGetValue("keep_alive", out long keepAlive)) { request.KeepAlive = keepAlive; } - if (requestProps.TryGetConvertedValue("truncate", out bool truncate)) + if (requestProps.TryGetValue("truncate", out bool truncate)) { request.Truncate = truncate; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj index 1efedb13f11..3426263d157 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj @@ -19,7 +19,6 @@ - true true true diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index f92fcfa3bc9..695a6fc620b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -11,7 +11,6 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; using OpenAI; using OpenAI.Chat; @@ -410,17 +409,17 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) if (options.AdditionalProperties is { Count: > 0 } additionalProperties) { - if (additionalProperties.TryGetConvertedValue(nameof(result.EndUserId), out string? endUserId)) + if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId)) { result.EndUserId = endUserId; } - if (additionalProperties.TryGetConvertedValue(nameof(result.IncludeLogProbabilities), out bool includeLogProbabilities)) + if (additionalProperties.TryGetValue(nameof(result.IncludeLogProbabilities), out bool includeLogProbabilities)) { result.IncludeLogProbabilities = includeLogProbabilities; } - if (additionalProperties.TryGetConvertedValue(nameof(result.LogitBiases), out IDictionary? logitBiases)) + if (additionalProperties.TryGetValue(nameof(result.LogitBiases), out IDictionary? logitBiases)) { foreach (KeyValuePair kvp in logitBiases!) { @@ -428,19 +427,19 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) } } - if (additionalProperties.TryGetConvertedValue(nameof(result.AllowParallelToolCalls), out bool allowParallelToolCalls)) + if (additionalProperties.TryGetValue(nameof(result.AllowParallelToolCalls), out bool allowParallelToolCalls)) { result.AllowParallelToolCalls = allowParallelToolCalls; } #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - if (additionalProperties.TryGetConvertedValue(nameof(result.Seed), out long seed)) + if (additionalProperties.TryGetValue(nameof(result.Seed), out long seed)) { result.Seed = seed; } #pragma warning restore OPENAI001 - if (additionalProperties.TryGetConvertedValue(nameof(result.TopLogProbabilityCount), out int topLogProbabilityCountInt)) + if (additionalProperties.TryGetValue(nameof(result.TopLogProbabilityCount), out int topLogProbabilityCountInt)) { result.TopLogProbabilityCount = topLogProbabilityCountInt; } @@ -488,7 +487,10 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) /// Converts an Extensions function to an OpenAI chat tool. private ChatTool ToOpenAIChatTool(AIFunction aiFunction) { - _ = aiFunction.Metadata.AdditionalProperties.TryGetConvertedValue("Strict", out bool strict); + bool? strict = + aiFunction.Metadata.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && + strictObj is bool strictValue ? + strictValue : null; BinaryData resultParameters = OpenAIChatToolJson.ZeroFunctionParametersSchema; @@ -643,7 +645,7 @@ private sealed class OpenAIChatToolJson new(toolCalls.Values) { ParticipantName = input.AuthorName } : new(input.Text) { ParticipantName = input.AuthorName }; - if (input.AdditionalProperties?.TryGetConvertedValue(nameof(message.Refusal), out string? refusal) is true) + if (input.AdditionalProperties?.TryGetValue(nameof(message.Refusal), out string? refusal) is true) { message.Refusal = refusal; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index e91394befdd..084e235df47 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; using OpenAI; using OpenAI.Embeddings; @@ -144,12 +143,12 @@ void IDisposable.Dispose() if (options?.AdditionalProperties is { Count: > 0 } additionalProperties) { // Allow per-instance dimensions to be overridden by a per-call property - if (additionalProperties.TryGetConvertedValue(nameof(openAIOptions.Dimensions), out int? dimensions)) + if (additionalProperties.TryGetValue(nameof(openAIOptions.Dimensions), out int? dimensions)) { openAIOptions.Dimensions = dimensions; } - if (additionalProperties.TryGetConvertedValue(nameof(openAIOptions.EndUserId), out string? endUserId)) + if (additionalProperties.TryGetValue(nameof(openAIOptions.EndUserId), out string? endUserId)) { openAIOptions.EndUserId = endUserId; } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 13e2d1229dd..5129ec9d160 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -11,7 +11,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.Shared.Collections; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -308,7 +307,7 @@ private static Dictionary> OrganizeStre _ = activity.SetTag(OpenTelemetryConsts.GenAI.Request.Temperature, temperature); } - if (options.AdditionalProperties?.TryGetConvertedValue("top_k", out double topK) is true) + if (options.AdditionalProperties?.TryGetValue("top_k", out double topK) is true) { _ = activity.SetTag(OpenTelemetryConsts.GenAI.Request.TopK, topK); } diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 31beec15fe6..bda7af37a5a 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -19,7 +19,6 @@ - true true false diff --git a/src/Shared/CollectionExtensions/CollectionExtensions.cs b/src/Shared/CollectionExtensions/CollectionExtensions.cs deleted file mode 100644 index 33196e6e771..00000000000 --- a/src/Shared/CollectionExtensions/CollectionExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -// 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.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; - -#pragma warning disable S108 // Nested blocks of code should not be left empty -#pragma warning disable S1067 // Expressions should not be too complex -#pragma warning disable SA1501 // Statement should not be on a single line - -#pragma warning disable CA1716 -namespace Microsoft.Shared.Collections; -#pragma warning restore CA1716 - -/// -/// Utilities to augment the basic collection types. -/// -#if !SHARED_PROJECT -[ExcludeFromCodeCoverage] -#endif - -internal static class CollectionExtensions -{ - /// Attempts to extract a typed value from the dictionary. - /// The dictionary to query. - /// The key to locate. - /// The value retrieved from the dictionary, if found; otherwise, default. - /// True if the value was found and converted to the requested type; otherwise, false. - /// - /// If a value is found for the key in the dictionary, but the value is not of the requested type but is - /// an object, the method will attempt to convert the object to the requested type. - /// is employed because these methods are primarily intended for use with primitives. - /// - public static bool TryGetConvertedValue(this IReadOnlyDictionary? input, string key, [NotNullWhen(true)] out T? value) - { - object? valueObject = null; - _ = input?.TryGetValue(key, out valueObject); - return TryConvertValue(valueObject, out value); - } - - private static bool TryConvertValue(object? obj, [NotNullWhen(true)] out T? value) - { - switch (obj) - { - case T t: - // The object is already of the requested type. Return it. - value = t; - return true; - - case IConvertible: - // The object is convertible; try to convert it to the requested type. Unfortunately, there's no - // convenient way to do this that avoids exceptions and that doesn't involve a ton of boilerplate, - // so we only try when the source object is at least an IConvertible, which is what ChangeType uses. - try - { - value = (T)Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); - return true; - } - catch (ArgumentException) { } - catch (InvalidCastException) { } - catch (FormatException) { } - catch (OverflowException) { } - break; - } - - // Unable to convert the object to the requested type. Fail. - value = default; - return false; - } -} diff --git a/src/Shared/CollectionExtensions/README.md b/src/Shared/CollectionExtensions/README.md deleted file mode 100644 index a732b7c36d4..00000000000 --- a/src/Shared/CollectionExtensions/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Collection Extensions - -`TryGetTypedValue` performs a ``TryGetValue` on a dictionary and then attempts to cast the value to the specified type. If the value is not of the specified type, false is returned. - -To use this in your project, add the following to your `.csproj` file: - -```xml - - true - -``` diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs index e71b2f431e8..a9a544c8ca8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.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.Collections.Generic; using Xunit; @@ -44,4 +45,49 @@ public void Comparer_OrdinalIgnoreCase() Assert.Equal("value5", d["Key3"]); Assert.Equal("value5", d["KEy3"]); } + + [Fact] + public void TryGetValue_Typed_ExtractsExpectedValue() + { + AssertFound(42, 42L); + AssertFound(42, 42.0); + AssertFound(42, 42f); + AssertFound(42, true); + AssertFound(42, "42"); + AssertFound(42, (object)42); + AssertFound(42.0, 42f); + AssertFound(42f, 42.0); + AssertFound(42m, 42.0f); + AssertFound(42L, 42); + AssertFound("42", "42"); + AssertFound("42", 42); + AssertFound("42", 42L); + AssertFound("42", 42.0); + AssertFound("42", 42f); + AssertFound(true, 1); + AssertFound(false, 0); + + AssertNotFound(42); + AssertNotFound(42); + + static void AssertFound(T1 input, T2 expected) + { + AdditionalPropertiesDictionary d = []; + d["key"] = input; + + Assert.True(d.TryGetValue("key", out T2? value)); + Assert.Equal(expected, value); + + Assert.False(d.TryGetValue("key2", out value)); + Assert.Equal(default, value); + } + + static void AssertNotFound(T1 input) + { + AdditionalPropertiesDictionary d = []; + d["key"] = input; + Assert.False(d.TryGetValue("key", out T2? value)); + Assert.Equal(default(T2), value); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index f19a19f3ce8..947deb2674d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -414,8 +414,7 @@ public async Task FunctionCallContent_NonStreaming() "type": "string" } } - }, - "strict": false + } } } ], @@ -529,8 +528,7 @@ public async Task FunctionCallContent_Streaming() "type": "string" } } - }, - "strict": false + } } } ],