Skip to content

Commit 76cbefc

Browse files
committed
Merge branch 'main' of https://github.com/dotnet/extensions into connector_id
2 parents 2a54c03 + 4e71161 commit 76cbefc

File tree

52 files changed

+2284
-140
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2284
-140
lines changed

eng/Versions.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup Label="Version settings">
3-
<MajorVersion>9</MajorVersion>
4-
<MinorVersion>10</MinorVersion>
3+
<MajorVersion>10</MajorVersion>
4+
<MinorVersion>0</MinorVersion>
55
<PatchVersion>0</PatchVersion>
66
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel>
77
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>

src/Generators/Microsoft.Gen.Metrics/Parser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ private bool CheckMethodReturnType(IMethodSymbol methodSymbol)
574574
returnType.TypeKind != TypeKind.Error)
575575
{
576576
// Make sure return type is not from existing known type
577-
Diag(DiagDescriptors.ErrorInvalidMethodReturnType, methodSymbol.ReturnType.GetLocation(), methodSymbol.Name);
577+
Diag(DiagDescriptors.ErrorInvalidMethodReturnType, returnType.GetLocation(), returnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
578578
return false;
579579
}
580580

src/Libraries/.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2936,7 +2936,7 @@ dotnet_diagnostic.S103.severity = suggestion
29362936
# Title : Files should not have too many lines of code
29372937
# Category : Major Code Smell
29382938
# Help Link: https://rules.sonarsource.com/csharp/RSPEC-104
2939-
dotnet_diagnostic.S104.severity = warning
2939+
dotnet_diagnostic.S104.severity = none
29402940

29412941
# Title : Finalizers should not throw exceptions
29422942
# Category : Blocker Bug

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Added protected copy constructors to options types (e.g. `ChatOptions`).
66
- Fixed `EmbeddingGeneratorOptions`/`SpeechToTextOptions` `Clone` methods to correctly copy all properties.
77
- Fixed `ToChatResponse` to not overwrite `ChatMessage/ChatResponse.CreatedAt` with older timestamps during coalescing.
8+
- Added `[Experimental]` support for background responses, such that non-streaming responses are allowed to be pollable, and such that responses and response updates can be tagged with continuation tokens to support later resumption.
89

910
## 9.9.1
1011

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Text.Json.Serialization;
78

89
namespace Microsoft.Extensions.AI;
@@ -25,8 +26,10 @@ protected ChatOptions(ChatOptions? other)
2526
}
2627

2728
AdditionalProperties = other.AdditionalProperties?.Clone();
29+
AllowBackgroundResponses = other.AllowBackgroundResponses;
2830
AllowMultipleToolCalls = other.AllowMultipleToolCalls;
2931
ConversationId = other.ConversationId;
32+
ContinuationToken = other.ContinuationToken;
3033
FrequencyPenalty = other.FrequencyPenalty;
3134
Instructions = other.Instructions;
3235
MaxOutputTokens = other.MaxOutputTokens;
@@ -155,6 +158,47 @@ protected ChatOptions(ChatOptions? other)
155158
[JsonIgnore]
156159
public IList<AITool>? Tools { get; set; }
157160

161+
/// <summary>Gets or sets a value indicating whether the background responses are allowed.</summary>
162+
/// <remarks>
163+
/// <para>
164+
/// Background responses allow running long-running operations or tasks asynchronously in the background that can be resumed by streaming APIs
165+
/// and polled for completion by non-streaming APIs.
166+
/// </para>
167+
/// <para>
168+
/// When this property is set to true, non-streaming APIs may start a background operation and return an initial
169+
/// response with a continuation token. Subsequent calls to the same API should be made in a polling manner with
170+
/// the continuation token to get the final result of the operation.
171+
/// </para>
172+
/// <para>
173+
/// When this property is set to true, streaming APIs may also start a background operation and begin streaming
174+
/// response updates until the operation is completed. If the streaming connection is interrupted, the
175+
/// continuation token obtained from the last update that has one should be supplied to a subsequent call to the same streaming API
176+
/// to resume the stream from the point of interruption and continue receiving updates until the operation is completed.
177+
/// </para>
178+
/// <para>
179+
/// This property only takes effect if the implementation it's used with supports background responses.
180+
/// If the implementation does not support background responses, this property will be ignored.
181+
/// </para>
182+
/// </remarks>
183+
[Experimental("MEAI001")]
184+
[JsonIgnore]
185+
public bool? AllowBackgroundResponses { get; set; }
186+
187+
/// <summary>Gets or sets the continuation token for resuming and getting the result of the chat response identified by this token.</summary>
188+
/// <remarks>
189+
/// This property is used for background responses that can be activated via the <see cref="AllowBackgroundResponses"/>
190+
/// property if the <see cref="IChatClient"/> implementation supports them.
191+
/// Streamed background responses, such as those returned by default by <see cref="IChatClient.GetStreamingResponseAsync"/>,
192+
/// can be resumed if interrupted. This means that a continuation token obtained from the <see cref="ChatResponseUpdate.ContinuationToken"/>
193+
/// of an update just before the interruption occurred can be passed to this property to resume the stream from the point of interruption.
194+
/// Non-streamed background responses, such as those returned by <see cref="IChatClient.GetResponseAsync"/>,
195+
/// can be polled for completion by obtaining the token from the <see cref="ChatResponse.ContinuationToken"/> property
196+
/// and passing it to this property on subsequent calls to <see cref="IChatClient.GetResponseAsync"/>.
197+
/// </remarks>
198+
[Experimental("MEAI001")]
199+
[JsonIgnore]
200+
public object? ContinuationToken { get; set; }
201+
158202
/// <summary>
159203
/// Gets or sets a callback responsible for creating the raw representation of the chat options from an underlying implementation.
160204
/// </summary>

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ public IList<ChatMessage> Messages
8888
/// <summary>Gets or sets usage details for the chat response.</summary>
8989
public UsageDetails? Usage { get; set; }
9090

91+
/// <summary>Gets or sets the continuation token for getting result of the background chat response.</summary>
92+
/// <remarks>
93+
/// <see cref="IChatClient"/> implementations that support background responses will return
94+
/// a continuation token if background responses are allowed in <see cref="ChatOptions.AllowBackgroundResponses"/>
95+
/// and the result of the response has not been obtained yet. If the response has completed and the result has been obtained,
96+
/// the token will be <see langword="null"/>.
97+
/// <para>
98+
/// This property should be used in conjunction with <see cref="ChatOptions.ContinuationToken"/> to
99+
/// continue to poll for the completion of the response. Pass this token to
100+
/// <see cref="ChatOptions.ContinuationToken"/> on subsequent calls to <see cref="IChatClient.GetResponseAsync"/>
101+
/// to poll for completion.
102+
/// </para>
103+
/// </remarks>
104+
[Experimental("MEAI001")]
105+
[JsonIgnore]
106+
public object? ContinuationToken { get; set; }
107+
91108
/// <summary>Gets or sets the raw representation of the chat response from an underlying implementation.</summary>
92109
/// <remarks>
93110
/// If a <see cref="ChatResponse"/> is created to represent some underlying object from another object
@@ -143,6 +160,7 @@ public ChatResponseUpdate[] ToChatResponseUpdates()
143160
ResponseId = ResponseId,
144161

145162
CreatedAt = message.CreatedAt ?? CreatedAt,
163+
ContinuationToken = ContinuationToken,
146164
};
147165
}
148166

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

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -306,26 +306,19 @@ private static void FinalizeResponse(ChatResponse response)
306306
private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse response)
307307
{
308308
// If there is no message created yet, or if the last update we saw had a different
309-
// message ID or role than the newest update, create a new message.
310-
ChatMessage message;
311-
var isNewMessage = false;
312-
if (response.Messages.Count == 0)
313-
{
314-
isNewMessage = true;
315-
}
316-
else if (update.MessageId is { Length: > 0 } updateMessageId
317-
&& response.Messages[response.Messages.Count - 1].MessageId is string lastMessageId
318-
&& updateMessageId != lastMessageId)
319-
{
320-
isNewMessage = true;
321-
}
322-
else if (update.Role is { } updateRole
323-
&& response.Messages[response.Messages.Count - 1].Role is { } lastRole
324-
&& updateRole != lastRole)
309+
// identifying parts, create a new message.
310+
bool isNewMessage = true;
311+
if (response.Messages.Count != 0)
325312
{
326-
isNewMessage = true;
313+
var lastMessage = response.Messages[response.Messages.Count - 1];
314+
isNewMessage =
315+
NotEmptyOrEqual(update.AuthorName, lastMessage.AuthorName) ||
316+
NotEmptyOrEqual(update.MessageId, lastMessage.MessageId) ||
317+
NotNullOrEqual(update.Role, lastMessage.Role);
327318
}
328319

320+
// Get the message to target, either a new one or the last ones.
321+
ChatMessage message;
329322
if (isNewMessage)
330323
{
331324
message = new(ChatRole.Assistant, []);
@@ -418,4 +411,12 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
418411
}
419412
}
420413
}
414+
415+
/// <summary>Gets whether both strings are not null/empty and not the same as each other.</summary>
416+
private static bool NotEmptyOrEqual(string? s1, string? s2) =>
417+
s1 is { Length: > 0 } str1 && s2 is { Length: > 0 } str2 && str1 != str2;
418+
419+
/// <summary>Gets whether two roles are not null and not the same as each other.</summary>
420+
private static bool NotNullOrEqual(ChatRole? r1, ChatRole? r2) =>
421+
r1.HasValue && r2.HasValue && r1.Value != r2.Value;
421422
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,21 @@ public IList<AIContent> Contents
136136
/// <inheritdoc/>
137137
public override string ToString() => Text;
138138

139+
/// <summary>Gets or sets the continuation token for resuming the streamed chat response of which this update is a part.</summary>
140+
/// <remarks>
141+
/// <see cref="IChatClient"/> implementations that support background responses will return
142+
/// a continuation token on each update if background responses are allowed in <see cref="ChatOptions.AllowBackgroundResponses"/>
143+
/// except of the last update, for which the token will be <see langword="null"/>.
144+
/// <para>
145+
/// This property should be used for stream resumption, where the continuation token of the latest received update should be
146+
/// passed to <see cref="ChatOptions.ContinuationToken"/> on subsequent calls to <see cref="IChatClient.GetStreamingResponseAsync"/>
147+
/// to resume streaming from the point of interruption.
148+
/// </para>
149+
/// </remarks>
150+
[Experimental("MEAI001")]
151+
[JsonIgnore]
152+
public object? ContinuationToken { get; set; }
153+
139154
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
140155
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
141156
private AIContent? ContentForDebuggerDisplay

src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ namespace Microsoft.Extensions.AI;
2121

2222
// These should be added in once they're no longer [Experimental]. If they're included while still
2323
// experimental, any JsonSerializerContext that includes AIContent will incur errors about using
24-
// experimental types in its source generated files.
24+
// experimental types in its source generated files. When [Experimental] is removed from these types,
25+
// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions
26+
// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed.
2527
// [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
2628
// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
2729
// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -749,11 +749,21 @@ private static string GetFunctionName(MethodInfo method)
749749
string name = SanitizeMemberName(method.Name);
750750

751751
const string AsyncSuffix = "Async";
752-
if (IsAsyncMethod(method) &&
753-
name.EndsWith(AsyncSuffix, StringComparison.Ordinal) &&
754-
name.Length > AsyncSuffix.Length)
752+
if (IsAsyncMethod(method))
755753
{
756-
name = name.Substring(0, name.Length - AsyncSuffix.Length);
754+
// If the method ends in "Async" or contains "Async_", remove the "Async".
755+
int asyncIndex = name.LastIndexOf(AsyncSuffix, StringComparison.Ordinal);
756+
if (asyncIndex > 0 &&
757+
(asyncIndex + AsyncSuffix.Length == name.Length ||
758+
((asyncIndex + AsyncSuffix.Length < name.Length) && (name[asyncIndex + AsyncSuffix.Length] == '_'))))
759+
{
760+
name =
761+
#if NET
762+
string.Concat(name.AsSpan(0, asyncIndex), name.AsSpan(asyncIndex + AsyncSuffix.Length));
763+
#else
764+
string.Concat(name.Substring(0, asyncIndex), name.Substring(asyncIndex + AsyncSuffix.Length));
765+
#endif
766+
}
757767
}
758768

759769
return name;
@@ -1105,16 +1115,37 @@ private record struct DescriptorKey(
11051115
/// Replaces non-alphanumeric characters in the identifier with the underscore character.
11061116
/// Primarily intended to remove characters produced by compiler-generated method name mangling.
11071117
/// </returns>
1108-
private static string SanitizeMemberName(string memberName) =>
1109-
InvalidNameCharsRegex().Replace(memberName, "_");
1118+
private static string SanitizeMemberName(string memberName)
1119+
{
1120+
// Handle compiler-generated names (local functions and lambdas)
1121+
// Local functions: <ContainingMethod>g__LocalFunctionName|ordinal_depth -> ContainingMethod_LocalFunctionName_ordinal_depth
1122+
// Lambdas: <ContainingMethod>b__ordinal_depth -> ContainingMethod_ordinal_depth
1123+
if (CompilerGeneratedNameRegex().Match(memberName) is { Success: true } match)
1124+
{
1125+
memberName = $"{match.Groups[1].Value}_{match.Groups[2].Value}";
1126+
}
1127+
1128+
// Replace all non-alphanumeric characters with underscores.
1129+
return InvalidNameCharsRegex().Replace(memberName, "_");
1130+
}
1131+
1132+
/// <summary>Regex that matches compiler-generated names (local functions and lambdas).</summary>
1133+
#if NET
1134+
[GeneratedRegex(@"^<([^>]+)>\w__(.+)")]
1135+
private static partial Regex CompilerGeneratedNameRegex();
1136+
#else
1137+
private static Regex CompilerGeneratedNameRegex() => _compilerGeneratedNameRegex;
1138+
private static readonly Regex _compilerGeneratedNameRegex = new(@"^<([^>]+)>\w__(.+)", RegexOptions.Compiled);
1139+
#endif
11101140

1111-
/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
1141+
/// <summary>Regex that flags any character other than ASCII digits or letters.</summary>
1142+
/// <remarks>Underscore isn't included so that sequences of underscores are replaced by a single one.</remarks>
11121143
#if NET
1113-
[GeneratedRegex("[^0-9A-Za-z_]")]
1144+
[GeneratedRegex("[^0-9A-Za-z]+")]
11141145
private static partial Regex InvalidNameCharsRegex();
11151146
#else
11161147
private static Regex InvalidNameCharsRegex() => _invalidNameCharsRegex;
1117-
private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z_]", RegexOptions.Compiled);
1148+
private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z]+", RegexOptions.Compiled);
11181149
#endif
11191150

11201151
/// <summary>Invokes the MethodInfo with the specified target object and arguments.</summary>

0 commit comments

Comments
 (0)