Skip to content

Commit 1eb963e

Browse files
SergeyMenshykhCopilotstephentoubwestey-m
authored
add support for background responses (#6854)
* add support for background responses Adds a model for supporting background responses (long-running operations), updates the chat completion model to use it, and integrates this functionality into OpenAIResponsesChatClient. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
1 parent 3d0419c commit 1eb963e

File tree

20 files changed

+1357
-23
lines changed

20 files changed

+1357
-23
lines changed

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/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
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.ComponentModel;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
using Microsoft.Shared.Diagnostics;
10+
11+
namespace Microsoft.Extensions.AI;
12+
13+
/// <summary>
14+
/// Represents a token used to resume, continue, or rehydrate an operation across multiple scenarios/calls,
15+
/// such as resuming a streamed response from a specific point or retrieving the result of a background operation.
16+
/// Subclasses of this class encapsulate all necessary information within the token to facilitate these actions.
17+
/// </summary>
18+
[JsonConverter(typeof(Converter))]
19+
[Experimental("MEAI001")]
20+
public class ResponseContinuationToken
21+
{
22+
/// <summary>Bytes representing this token.</summary>
23+
private readonly ReadOnlyMemory<byte> _bytes;
24+
25+
/// <summary>Initializes a new instance of the <see cref="ResponseContinuationToken"/> class.</summary>
26+
protected ResponseContinuationToken()
27+
{
28+
}
29+
30+
/// <summary>Initializes a new instance of the <see cref="ResponseContinuationToken"/> class.</summary>
31+
/// <param name="bytes">Bytes to create the token from.</param>
32+
protected ResponseContinuationToken(ReadOnlyMemory<byte> bytes)
33+
{
34+
_bytes = bytes;
35+
}
36+
37+
/// <summary>Create a new instance of <see cref="ResponseContinuationToken"/> from the provided <paramref name="bytes"/>.
38+
/// </summary>
39+
/// <param name="bytes">Bytes representing the <see cref="ResponseContinuationToken"/>.</param>
40+
/// <returns>A <see cref="ResponseContinuationToken"/> equivalent to the one from which
41+
/// the original<see cref="ResponseContinuationToken"/> bytes were obtained.</returns>
42+
public static ResponseContinuationToken FromBytes(ReadOnlyMemory<byte> bytes) => new(bytes);
43+
44+
/// <summary>Gets the bytes representing this <see cref="ResponseContinuationToken"/>.</summary>
45+
/// <returns>Bytes representing the <see cref="ResponseContinuationToken"/>.</returns>"/>
46+
public virtual ReadOnlyMemory<byte> ToBytes() => _bytes;
47+
48+
/// <summary>Provides a <see cref="JsonConverter{ResponseContinuationToken}"/> for serializing <see cref="ResponseContinuationToken"/> instances.</summary>
49+
[EditorBrowsable(EditorBrowsableState.Never)]
50+
[Experimental("MEAI001")]
51+
public sealed class Converter : JsonConverter<ResponseContinuationToken>
52+
{
53+
/// <inheritdoc/>
54+
public override ResponseContinuationToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
55+
{
56+
return ResponseContinuationToken.FromBytes(reader.GetBytesFromBase64());
57+
}
58+
59+
/// <inheritdoc/>
60+
public override void Write(Utf8JsonWriter writer, ResponseContinuationToken value, JsonSerializerOptions options)
61+
{
62+
_ = Throw.IfNull(writer);
63+
_ = Throw.IfNull(value);
64+
65+
writer.WriteBase64StringValue(value.ToBytes().Span);
66+
}
67+
}
68+
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private static JsonSerializerOptions CreateDefaultOptions()
135135
[JsonSerializable(typeof(McpServerToolResultContent))]
136136
[JsonSerializable(typeof(McpServerToolApprovalRequestContent))]
137137
[JsonSerializable(typeof(McpServerToolApprovalResponseContent))]
138-
138+
[JsonSerializable(typeof(ResponseContinuationToken))]
139139
[EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead.
140140
private sealed partial class JsonContext : JsonSerializerContext;
141141

src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public static ChatResponse AsChatResponse(this OpenAIResponse response, Response
6464
/// <exception cref="ArgumentNullException"><paramref name="responseUpdates"/> is <see langword="null"/>.</exception>
6565
public static IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAsync(
6666
this IAsyncEnumerable<StreamingResponseUpdate> responseUpdates, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default) =>
67-
OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, cancellationToken);
67+
OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, cancellationToken: cancellationToken);
6868

6969
/// <summary>Creates an OpenAI <see cref="OpenAIResponse"/> from a <see cref="ChatResponse"/>.</summary>
7070
/// <param name="response">The response to convert.</param>

0 commit comments

Comments
 (0)