Skip to content

Commit a3d136b

Browse files
jeffhandleyjoperezr
authored andcommitted
Merged PR 49950: Update Microsoft.Extensions.AI and Microsoft.Extensions.AI.Abstractions from main
This updates the release/9.5 branch with the latest from main for the MEAI/MEAI.Abstractions libraries along with compensating changes on the adapter libraries. ---- #### AI description (iteration 1) #### PR Classification This pull request implements a comprehensive API update and extensive internal refactoring for chat client, schema transformation, and embedding functionalities. #### PR Summary The changes standardize and improve internal handling of chat options, JSON schema transformation, and embedding representations while updating related tests and logging, resulting in clearer APIs and more robust behavior. - **`src/Libraries/Microsoft.Extensions.AI.OpenAI/` & `src/Libraries/Microsoft.Extensions.AI.AzureAIInference/`**: Clients now use a shared `AIJsonSchemaTransformCache` for transforming and validating JSON schemas, and additional properties (e.g., refusals) are handled more cleanly. - **`src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/`**: New utilities and types (e.g., `AIJsonSchemaTransformOptions`, `AIJsonSchemaTransformContext`, and `AIJsonSchemaTransformCache`) have been introduced to support schema conversion and enforcement. - **`src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/`**: Embedding types are updated with standardized discriminator names (e.g., `"float16"`, `"float32"`, `"float64"`) and a new `BinaryEmbedding` class is added for bit-based embeddings. - **`ChatOptions` & Structured Output Extensions**: The API now supports a `RawRepresentationFactory` and renames the JSON schema parameter to `useJsonSchemaResponseFormat` for clarity and consistency. - **Test and Logging Updates**: Multiple test files and logging extension builders have been updated to reflect the new behaviors and internal changes. <!-- GitOpsUserAgent=GitOps.Apps.Server.pullrequestcopilot -->
2 parents 3ac172a + 49bb2c5 commit a3d136b

File tree

38 files changed

+2373
-426
lines changed

38 files changed

+2373
-426
lines changed

eng/MSBuild/LegacySupport.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\RequiredMemberAttribute\*.cs" LinkBase="LegacySupport\RequiredMemberAttribute" />
88
</ItemGroup>
99

10+
<ItemGroup Condition="'$(InjectSystemIndexOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
11+
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\SystemIndex\*.cs" LinkBase="LegacySupport\SystemIndex" />
12+
</ItemGroup>
13+
1014
<ItemGroup Condition="'$(InjectDiagnosticAttributesOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1')">
1115
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\DiagnosticAttributes\*.cs" LinkBase="LegacySupport\DiagnosticAttributes" />
1216
</ItemGroup>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Runtime.CompilerServices;
6+
7+
#pragma warning disable CS0436 // Type conflicts with imported type
8+
#pragma warning disable S3427 // Method overloads with default parameter values should not overlap
9+
#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text
10+
#pragma warning disable IDE0011 // Add braces
11+
#pragma warning disable SA1623 // Property summary documentation should match accessors
12+
#pragma warning disable IDE0023 // Use block body for conversion operator
13+
#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one
14+
#pragma warning disable LA0001 // Use the 'Microsoft.Shared.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance
15+
#pragma warning disable CA1305 // Specify IFormatProvider
16+
17+
namespace System
18+
{
19+
internal readonly struct Index : IEquatable<Index>
20+
{
21+
private readonly int _value;
22+
23+
/// <summary>Construct an Index using a value and indicating if the index is from the start or from the end.</summary>
24+
/// <param name="value">The index value. it has to be zero or positive number.</param>
25+
/// <param name="fromEnd">Indicating if the index is from the start or from the end.</param>
26+
/// <remarks>
27+
/// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element.
28+
/// </remarks>
29+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
30+
public Index(int value, bool fromEnd = false)
31+
{
32+
if (value < 0)
33+
{
34+
ThrowValueArgumentOutOfRange_NeedNonNegNumException();
35+
}
36+
37+
if (fromEnd)
38+
_value = ~value;
39+
else
40+
_value = value;
41+
}
42+
43+
// The following private constructors mainly created for perf reason to avoid the checks
44+
private Index(int value)
45+
{
46+
_value = value;
47+
}
48+
49+
/// <summary>Create an Index pointing at first element.</summary>
50+
public static Index Start => new Index(0);
51+
52+
/// <summary>Create an Index pointing at beyond last element.</summary>
53+
public static Index End => new Index(~0);
54+
55+
/// <summary>Create an Index from the start at the position indicated by the value.</summary>
56+
/// <param name="value">The index value from the start.</param>
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
58+
public static Index FromStart(int value)
59+
{
60+
if (value < 0)
61+
{
62+
ThrowValueArgumentOutOfRange_NeedNonNegNumException();
63+
}
64+
65+
return new Index(value);
66+
}
67+
68+
/// <summary>Create an Index from the end at the position indicated by the value.</summary>
69+
/// <param name="value">The index value from the end.</param>
70+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
71+
public static Index FromEnd(int value)
72+
{
73+
if (value < 0)
74+
{
75+
ThrowValueArgumentOutOfRange_NeedNonNegNumException();
76+
}
77+
78+
return new Index(~value);
79+
}
80+
81+
/// <summary>Returns the index value.</summary>
82+
public int Value
83+
{
84+
get
85+
{
86+
if (_value < 0)
87+
return ~_value;
88+
else
89+
return _value;
90+
}
91+
}
92+
93+
/// <summary>Indicates whether the index is from the start or the end.</summary>
94+
public bool IsFromEnd => _value < 0;
95+
96+
/// <summary>Calculate the offset from the start using the giving collection length.</summary>
97+
/// <param name="length">The length of the collection that the Index will be used with. length has to be a positive value.</param>
98+
/// <remarks>
99+
/// For performance reason, we don't validate the input length parameter and the returned offset value against negative values.
100+
/// we don't validate either the returned offset is greater than the input length.
101+
/// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and
102+
/// then used to index a collection will get out of range exception which will be same affect as the validation.
103+
/// </remarks>
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
public int GetOffset(int length)
106+
{
107+
int offset = _value;
108+
if (IsFromEnd)
109+
{
110+
// offset = length - (~value)
111+
// offset = length + (~(~value) + 1)
112+
// offset = length + value + 1
113+
114+
offset += length + 1;
115+
}
116+
117+
return offset;
118+
}
119+
120+
/// <summary>Indicates whether the current Index object is equal to another object of the same type.</summary>
121+
/// <param name="value">An object to compare with this object.</param>
122+
public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value;
123+
124+
/// <summary>Indicates whether the current Index object is equal to another Index object.</summary>
125+
/// <param name="other">An object to compare with this object.</param>
126+
public bool Equals(Index other) => _value == other._value;
127+
128+
/// <summary>Returns the hash code for this instance.</summary>
129+
public override int GetHashCode() => _value;
130+
131+
/// <summary>Converts integer number to an Index.</summary>
132+
public static implicit operator Index(int value) => FromStart(value);
133+
134+
/// <summary>Converts the value of the current Index object to its equivalent string representation.</summary>
135+
public override string ToString()
136+
{
137+
if (IsFromEnd)
138+
return ToStringFromEnd();
139+
140+
return ((uint)Value).ToString();
141+
}
142+
143+
private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException()
144+
{
145+
throw new ArgumentOutOfRangeException("value", "value must be non-negative");
146+
}
147+
148+
private string ToStringFromEnd()
149+
{
150+
#if (!NETSTANDARD2_0 && !NETFRAMEWORK)
151+
Span<char> span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value
152+
bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten);
153+
span[0] = '^';
154+
return new string(span.Slice(0, charsWritten + 1));
155+
#else
156+
return '^' + Value.ToString();
157+
#endif
158+
}
159+
}
160+
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Text.Json.Serialization;
67

@@ -121,6 +122,26 @@ public string? ChatThreadId
121122
[JsonIgnore]
122123
public IList<AITool>? Tools { get; set; }
123124

125+
/// <summary>
126+
/// Gets or sets a callback responsible of creating the raw representation of the chat options from an underlying implementation.
127+
/// </summary>
128+
/// <remarks>
129+
/// The underlying <see cref="IChatClient" /> implementation may have its own representation of options.
130+
/// When <see cref="IChatClient.GetResponseAsync" /> or <see cref="IChatClient.GetStreamingResponseAsync" />
131+
/// is invoked with a <see cref="ChatOptions" />, that implementation may convert the provided options into
132+
/// its own representation in order to use it while performing the operation. For situations where a consumer knows
133+
/// which concrete <see cref="IChatClient" /> is being used and how it represents options, a new instance of that
134+
/// implementation-specific options type may be returned by this callback, for the <see cref="IChatClient" />
135+
/// implementation to use instead of creating a new instance. Such implementations may mutate the supplied options
136+
/// instance further based on other settings supplied on this <see cref="ChatOptions" /> instance or from other inputs,
137+
/// like the enumerable of <see cref="ChatMessage"/>s, therefore, its **strongly recommended** to not return shared instances
138+
/// and instead make the callback return a new instance per each call.
139+
/// This is typically used to set an implementation-specific setting that isn't otherwise exposed from the strongly-typed
140+
/// properties on <see cref="ChatOptions" />.
141+
/// </remarks>
142+
[JsonIgnore]
143+
public Func<IChatClient, object?>? RawRepresentationFactory { get; set; }
144+
124145
/// <summary>Gets or sets any additional properties associated with the options.</summary>
125146
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
126147

@@ -147,6 +168,7 @@ public virtual ChatOptions Clone()
147168
ModelId = ModelId,
148169
AllowMultipleToolCalls = AllowMultipleToolCalls,
149170
ToolMode = ToolMode,
171+
RawRepresentationFactory = RawRepresentationFactory,
150172
AdditionalProperties = AdditionalProperties?.Clone(),
151173
};
152174

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Collections;
7+
using System.ComponentModel;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
using Microsoft.Shared.Diagnostics;
11+
12+
namespace Microsoft.Extensions.AI;
13+
14+
/// <summary>Represents an embedding composed of a bit vector.</summary>
15+
public sealed class BinaryEmbedding : Embedding
16+
{
17+
/// <summary>The embedding vector this embedding represents.</summary>
18+
private BitArray _vector;
19+
20+
/// <summary>Initializes a new instance of the <see cref="BinaryEmbedding"/> class with the embedding vector.</summary>
21+
/// <param name="vector">The embedding vector this embedding represents.</param>
22+
/// <exception cref="ArgumentNullException"><paramref name="vector"/> is <see langword="null"/>.</exception>
23+
public BinaryEmbedding(BitArray vector)
24+
{
25+
_vector = Throw.IfNull(vector);
26+
}
27+
28+
/// <summary>Gets or sets the embedding vector this embedding represents.</summary>
29+
[JsonConverter(typeof(VectorConverter))]
30+
public BitArray Vector
31+
{
32+
get => _vector;
33+
set => _vector = Throw.IfNull(value);
34+
}
35+
36+
/// <inheritdoc />
37+
[JsonIgnore]
38+
public override int Dimensions => _vector.Length;
39+
40+
/// <summary>Provides a <see cref="JsonConverter{BitArray}"/> for serializing <see cref="BitArray"/> instances.</summary>
41+
[EditorBrowsable(EditorBrowsableState.Never)]
42+
public sealed class VectorConverter : JsonConverter<BitArray>
43+
{
44+
/// <inheritdoc/>
45+
public override BitArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
46+
{
47+
_ = Throw.IfNull(typeToConvert);
48+
_ = Throw.IfNull(options);
49+
50+
if (reader.TokenType != JsonTokenType.String)
51+
{
52+
throw new JsonException("Expected string property.");
53+
}
54+
55+
ReadOnlySpan<byte> utf8;
56+
byte[]? tmpArray = null;
57+
if (!reader.HasValueSequence && !reader.ValueIsEscaped)
58+
{
59+
utf8 = reader.ValueSpan;
60+
}
61+
else
62+
{
63+
// This path should be rare.
64+
int length = reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length;
65+
tmpArray = ArrayPool<byte>.Shared.Rent(length);
66+
utf8 = tmpArray.AsSpan(0, reader.CopyString(tmpArray));
67+
}
68+
69+
BitArray result = new(utf8.Length);
70+
71+
for (int i = 0; i < utf8.Length; i++)
72+
{
73+
result[i] = utf8[i] switch
74+
{
75+
(byte)'0' => false,
76+
(byte)'1' => true,
77+
_ => throw new JsonException("Expected binary character sequence.")
78+
};
79+
}
80+
81+
if (tmpArray is not null)
82+
{
83+
ArrayPool<byte>.Shared.Return(tmpArray);
84+
}
85+
86+
return result;
87+
}
88+
89+
/// <inheritdoc/>
90+
public override void Write(Utf8JsonWriter writer, BitArray value, JsonSerializerOptions options)
91+
{
92+
_ = Throw.IfNull(writer);
93+
_ = Throw.IfNull(value);
94+
_ = Throw.IfNull(options);
95+
96+
int length = value.Length;
97+
98+
byte[] tmpArray = ArrayPool<byte>.Shared.Rent(length);
99+
100+
Span<byte> utf8 = tmpArray.AsSpan(0, length);
101+
for (int i = 0; i < utf8.Length; i++)
102+
{
103+
utf8[i] = value[i] ? (byte)'1' : (byte)'0';
104+
}
105+
106+
writer.WriteStringValue(utf8);
107+
108+
ArrayPool<byte>.Shared.Return(tmpArray);
109+
}
110+
}
111+
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Text.Json.Serialization;
67

78
namespace Microsoft.Extensions.AI;
89

910
/// <summary>Represents an embedding generated by a <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/>.</summary>
1011
/// <remarks>This base class provides metadata about the embedding. Derived types provide the concrete data contained in the embedding.</remarks>
1112
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
13+
[JsonDerivedType(typeof(BinaryEmbedding), typeDiscriminator: "binary")]
14+
[JsonDerivedType(typeof(Embedding<byte>), typeDiscriminator: "uint8")]
15+
[JsonDerivedType(typeof(Embedding<sbyte>), typeDiscriminator: "int8")]
1216
#if NET
13-
[JsonDerivedType(typeof(Embedding<Half>), typeDiscriminator: "halves")]
17+
[JsonDerivedType(typeof(Embedding<Half>), typeDiscriminator: "float16")]
1418
#endif
15-
[JsonDerivedType(typeof(Embedding<float>), typeDiscriminator: "floats")]
16-
[JsonDerivedType(typeof(Embedding<double>), typeDiscriminator: "doubles")]
17-
[JsonDerivedType(typeof(Embedding<byte>), typeDiscriminator: "bytes")]
18-
[JsonDerivedType(typeof(Embedding<sbyte>), typeDiscriminator: "sbytes")]
19+
[JsonDerivedType(typeof(Embedding<float>), typeDiscriminator: "float32")]
20+
[JsonDerivedType(typeof(Embedding<double>), typeDiscriminator: "float64")]
21+
[DebuggerDisplay("Dimensions = {Dimensions}")]
1922
public class Embedding
2023
{
2124
/// <summary>Initializes a new instance of the <see cref="Embedding"/> class.</summary>
@@ -26,6 +29,13 @@ protected Embedding()
2629
/// <summary>Gets or sets a timestamp at which the embedding was created.</summary>
2730
public DateTimeOffset? CreatedAt { get; set; }
2831

32+
/// <summary>Gets the dimensionality of the embedding vector.</summary>
33+
/// <remarks>
34+
/// This value corresponds to the number of elements in the embedding vector.
35+
/// </remarks>
36+
[JsonIgnore]
37+
public virtual int Dimensions { get; }
38+
2939
/// <summary>Gets or sets the model ID using in the creation of the embedding.</summary>
3040
public string? ModelId { get; set; }
3141

src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/Embedding{T}.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Text.Json.Serialization;
56

67
namespace Microsoft.Extensions.AI;
78

@@ -19,4 +20,8 @@ public Embedding(ReadOnlyMemory<T> vector)
1920

2021
/// <summary>Gets or sets the embedding vector this embedding represents.</summary>
2122
public ReadOnlyMemory<T> Vector { get; set; }
23+
24+
/// <inheritdoc />
25+
[JsonIgnore]
26+
public override int Dimensions => Vector.Length;
2227
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<InjectSharedEmptyCollections>true</InjectSharedEmptyCollections>
2929
<InjectStringHashOnLegacy>true</InjectStringHashOnLegacy>
3030
<InjectStringSyntaxAttributeOnLegacy>true</InjectStringSyntaxAttributeOnLegacy>
31+
<InjectSystemIndexOnLegacy>true</InjectSystemIndexOnLegacy>
3132
</PropertyGroup>
3233

3334
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">

0 commit comments

Comments
 (0)