From 678a22a5ea8053f38200e5b72a1df4475159e585 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 25 Sep 2025 20:40:05 +0100 Subject: [PATCH 01/41] add support for background responses --- .../BackgroundResponsesOptions.cs | 33 + .../CHANGELOG.md | 5 + .../ChatCompletion/ChatClientExtensions.cs | 49 ++ .../ChatCompletion/ChatOptions.cs | 25 + .../ChatCompletion/ChatResponse.cs | 15 + .../ChatCompletion/ChatResponseUpdate.cs | 13 + .../Microsoft.Extensions.AI.Abstractions.json | 797 ++++++++++++++++-- .../ResumptionToken.cs | 67 ++ ...icrosoftExtensionsAIResponsesExtensions.cs | 2 +- .../OpenAIResponsesChatClient.cs | 144 +++- .../OpenAIResponsesContinuationToken.cs | 106 +++ .../FunctionInvokingChatClient.cs | 13 + .../Microsoft.Extensions.AI.csproj | 2 +- .../BackgroundResponsesOptionsTests.cs | 30 + .../ChatClientExtensionsTests.cs | 135 +++ .../ChatCompletion/ChatOptionsTests.cs | 28 + .../ResumptionTokenTests.cs | 37 + .../TestJsonSerializerContext.cs | 1 + .../HttpHandlerExpectedInput.cs | 22 + .../VerbatimHttpHandler.cs | 61 +- .../OpenAIResponseClientIntegrationTests.cs | 139 +++ .../OpenAIResponseClientTests.cs | 563 +++++++++++++ .../FunctionInvokingChatClientTests.cs | 38 + 23 files changed, 2211 insertions(+), 114 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/BackgroundResponsesOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/ResumptionToken.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/BackgroundResponsesOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ResumptionTokenTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Integration.Tests/HttpHandlerExpectedInput.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/BackgroundResponsesOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/BackgroundResponsesOptions.cs new file mode 100644 index 00000000000..b59a9c24674 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/BackgroundResponsesOptions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// Options for configuring background responses. +[Experimental("MEAI001")] +public sealed class BackgroundResponsesOptions +{ + /// Initializes a new instance of the class. + public BackgroundResponsesOptions() + { + } + + /// Initializes a new instance of the class. + /// The options to initialize from. + public BackgroundResponsesOptions(BackgroundResponsesOptions options) + { + _ = Throw.IfNull(options); + + Allow = options.Allow; + } + + /// Gets or sets a value indicating whether the background responses are allowed. + /// + /// This property only takes effect if the API it's used with supports background responses. + /// If the API does not support background responses, this property will be ignored. + /// + public bool? Allow { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 8a0e875557a..27ea62f90cb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -6,6 +6,11 @@ - Added new `AITool.GetService` virtual method. - Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content. - Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export. +- Added `[Experimental]` `ResumptionToken` to represent a base class for continuation tokens. +- Added `[Experimental]` `BackgroundResponsesOptions` to represent options for background responses. +- Added `[Experimental]` `ChatOptions.BackgroundResponsesOptions` and `ChatOptions.ContinuationToken` properties specify continuation token and options for background responses. +- Added `[Experimental]` `ChatResponse.ContinuationToken` and `ChatResponseUpdate.ContinuationToken` to return continuation token for background responses. +- Added `[Experimental]` `GetResponseAsync` and `GetStreamingResponseAsync` extension methods for `IChatClient` to continue background responses. ## 9.9.0 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientExtensions.cs index 81fd4f97778..20634370c25 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -120,6 +121,30 @@ public static Task GetResponseAsync( return client.GetResponseAsync([chatMessage], options, cancellationToken); } + /// Gets a background response identified by the specified continuation token. + /// The chat client. + /// The continuation token. + /// The chat options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The background response. + /// is . + /// is . + [Experimental("MEAI001")] + public static Task GetResponseAsync( + this IChatClient client, + ResumptionToken continuationToken, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(client); + _ = Throw.IfNull(continuationToken); + + ChatOptions chatOptions = options ?? new(); + chatOptions.ContinuationToken = continuationToken; + + return client.GetResponseAsync([], chatOptions, cancellationToken); + } + /// Sends a user chat text message and streams the response messages. /// The chat client. /// The text content for the chat message to send. @@ -159,4 +184,28 @@ public static IAsyncEnumerable GetStreamingResponseAsync( return client.GetStreamingResponseAsync([chatMessage], options, cancellationToken); } + + /// Gets a background streamed response identified by the specified continuation token and streams its messages. + /// The chat client. + /// The continuation token. + /// The chat options to configure the request. + /// The to monitor for cancellation requests. The default is . + /// The background response messages. + /// is . + /// is . + [Experimental("MEAI001")] + public static IAsyncEnumerable GetStreamingResponseAsync( + this IChatClient client, + ResumptionToken continuationToken, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(client); + _ = Throw.IfNull(continuationToken); + + ChatOptions chatOptions = options ?? new(); + chatOptions.ContinuationToken = continuationToken; + + return client.GetStreamingResponseAsync([], chatOptions, cancellationToken); + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 0be912430fa..9a09fc3a6d6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Microsoft.Extensions.AI; @@ -115,6 +116,24 @@ public class ChatOptions [JsonIgnore] public IList? Tools { get; set; } + /// Gets or sets the options for configuring background responses. + [Experimental("MEAI001")] + public BackgroundResponsesOptions? BackgroundResponsesOptions { get; set; } + + /// Gets or sets the continuation token for resuming and getting result of the chat response identified by this token. + /// + /// This property is used for background responses that can be activated via the + /// property if the implementation supports them. + /// Streamed background responses, such as those returned by default by , + /// can be resumed if interrupted. This means that a continuation token obtained from the + /// of an update just before the interruption occurred can be passed to this property to resume the stream from the point of interruption. + /// Non-streamed background responses, such as those returned by , + /// can be polled for completion by obtaining the token from the property + /// and passing it to this property on subsequent calls to . + /// + [Experimental("MEAI001")] + public ResumptionToken? ContinuationToken { get; set; } + /// /// Gets or sets a callback responsible for creating the raw representation of the chat options from an underlying implementation. /// @@ -152,6 +171,7 @@ public virtual ChatOptions Clone() AdditionalProperties = AdditionalProperties?.Clone(), AllowMultipleToolCalls = AllowMultipleToolCalls, ConversationId = ConversationId, + ContinuationToken = ContinuationToken, FrequencyPenalty = FrequencyPenalty, Instructions = Instructions, MaxOutputTokens = MaxOutputTokens, @@ -166,6 +186,11 @@ public virtual ChatOptions Clone() TopP = TopP, }; + if (BackgroundResponsesOptions is { } bros) + { + options.BackgroundResponsesOptions = new BackgroundResponsesOptions(bros); + } + if (StopSequences is not null) { options.StopSequences = new List(StopSequences); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs index 0889fed17d6..a24a51f4772 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs @@ -88,6 +88,20 @@ public IList Messages /// Gets or sets usage details for the chat response. public UsageDetails? Usage { get; set; } + /// Gets or sets the continuation token for getting result of the background chat response. + /// + /// implementations that support background responses will return + /// a continuation token if background responses are enabled in + /// and the result of the response has not been obtained yet. Otherwise, the token will be . + /// + /// This property should be used in conjunction with to + /// continue or polling for the completion the response. Pass this token to + /// on subsequent calls to + /// to poll for completion. + /// + /// + public ResumptionToken? ContinuationToken { get; set; } + /// Gets or sets the raw representation of the chat response from an underlying implementation. /// /// If a is created to represent some underlying object from another object @@ -143,6 +157,7 @@ public ChatResponseUpdate[] ToChatResponseUpdates() ResponseId = ResponseId, CreatedAt = message.CreatedAt ?? CreatedAt, + ContinuationToken = ContinuationToken, }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index bea91f97ed9..0e7d3351194 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -141,6 +141,19 @@ public IList Contents /// public override string ToString() => Text; + /// Gets or sets the continuation token for resuming the streamed chat response of which this update is a part. + /// + /// implementations that support background responses will return + /// a continuation token on each update if background responses are enabled in + /// and not all updates for the response have been streamed yet. Otherwise, the token will be . + /// + /// This property should be used for stream resumption, where the continuation token of the latest received update should be + /// passed to on subsequent calls to + /// to resume streaming from the point of interruption. + /// + /// + public ResumptionToken? ContinuationToken { get; set; } + /// Gets a object to display in the debugger display. [DebuggerBrowsable(DebuggerBrowsableState.Never)] private AIContent? ContentForDebuggerDisplay 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 73799733746..56de1ce31f3 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 @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.AI.Abstractions, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.AI.Abstractions, Version=9.10.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "sealed class Microsoft.Extensions.AI.AdditionalPropertiesDictionary : Microsoft.Extensions.AI.AdditionalPropertiesDictionary", @@ -134,11 +134,11 @@ ], "Properties": [ { - "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.AIAnnotation.AnnotatedRegions { get; set; }", + "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.AIAnnotation.AdditionalProperties { get; set; }", "Stage": "Stable" }, { - "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.AIAnnotation.AdditionalProperties { get; set; }", + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.AIAnnotation.AnnotatedRegions { get; set; }", "Stage": "Stable" }, { @@ -180,15 +180,15 @@ "Stage": "Stable" }, { - "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.AIFunction.InvokeAsync(Microsoft.Extensions.AI.AIFunctionArguments? arguments = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Member": "Microsoft.Extensions.AI.AIFunctionDeclaration Microsoft.Extensions.AI.AIFunction.AsDeclarationOnly();", "Stage": "Stable" }, { - "Member": "abstract System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.AIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);", + "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.AIFunction.InvokeAsync(Microsoft.Extensions.AI.AIFunctionArguments? arguments = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Stable" }, { - "Member": "Microsoft.Extensions.AI.AIFunctionDeclaration Microsoft.Extensions.AI.AIFunction.AsDeclarationOnly();", + "Member": "abstract System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.AIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);", "Stage": "Stable" } ], @@ -203,26 +203,6 @@ } ] }, - { - "Type": "abstract class Microsoft.Extensions.AI.AIFunctionDeclaration : Microsoft.Extensions.AI.AITool", - "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.AIFunctionDeclaration.AIFunctionDeclaration();", - "Stage": "Stable" - } - ], - "Properties": [ - { - "Member": "virtual System.Text.Json.JsonElement Microsoft.Extensions.AI.AIFunctionDeclaration.JsonSchema { get; }", - "Stage": "Stable" - }, - { - "Member": "virtual System.Text.Json.JsonElement? Microsoft.Extensions.AI.AIFunctionDeclaration.ReturnJsonSchema { get; }", - "Stage": "Stable" - } - ] - }, { "Type": "class Microsoft.Extensions.AI.AIFunctionArguments : System.Collections.Generic.IDictionary, System.Collections.Generic.ICollection>, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable, System.Collections.Generic.IReadOnlyDictionary, System.Collections.Generic.IReadOnlyCollection>", "Stage": "Stable", @@ -299,6 +279,26 @@ } ] }, + { + "Type": "abstract class Microsoft.Extensions.AI.AIFunctionDeclaration : Microsoft.Extensions.AI.AITool", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.AIFunctionDeclaration.AIFunctionDeclaration();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "virtual System.Text.Json.JsonElement Microsoft.Extensions.AI.AIFunctionDeclaration.JsonSchema { get; }", + "Stage": "Stable" + }, + { + "Member": "virtual System.Text.Json.JsonElement? Microsoft.Extensions.AI.AIFunctionDeclaration.ReturnJsonSchema { get; }", + "Stage": "Stable" + } + ] + }, { "Type": "static class Microsoft.Extensions.AI.AIFunctionFactory", "Stage": "Stable", @@ -723,6 +723,16 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.ApprovalRequiredAIFunction : Microsoft.Extensions.AI.DelegatingAIFunction", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ApprovalRequiredAIFunction.ApprovalRequiredAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);", + "Stage": "Experimental" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.AutoChatToolMode : Microsoft.Extensions.AI.ChatToolMode", "Stage": "Stable", @@ -741,6 +751,26 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.BackgroundResponsesOptions", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.BackgroundResponsesOptions.BackgroundResponsesOptions();", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.BackgroundResponsesOptions.BackgroundResponsesOptions(Microsoft.Extensions.AI.BackgroundResponsesOptions options);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "bool? Microsoft.Extensions.AI.BackgroundResponsesOptions.Allow { get; set; }", + "Stage": "Experimental" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.BinaryEmbedding : Microsoft.Extensions.AI.Embedding", "Stage": "Stable", @@ -799,6 +829,10 @@ "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.ChatClientExtensions.GetResponseAsync(this Microsoft.Extensions.AI.IChatClient client, Microsoft.Extensions.AI.ChatMessage chatMessage, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Stable" }, + { + "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.ChatClientExtensions.GetResponseAsync(this Microsoft.Extensions.AI.IChatClient client, Microsoft.Extensions.AI.ResumptionToken continuationToken, Microsoft.Extensions.AI.BackgroundResponsesOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, { "Member": "static TService? Microsoft.Extensions.AI.ChatClientExtensions.GetService(this Microsoft.Extensions.AI.IChatClient client, object? serviceKey = null);", "Stage": "Stable" @@ -810,6 +844,10 @@ { "Member": "static System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ChatClientExtensions.GetStreamingResponseAsync(this Microsoft.Extensions.AI.IChatClient client, Microsoft.Extensions.AI.ChatMessage chatMessage, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Stable" + }, + { + "Member": "static System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ChatClientExtensions.GetStreamingResponseAsync(this Microsoft.Extensions.AI.IChatClient client, Microsoft.Extensions.AI.ResumptionToken continuationToken, Microsoft.Extensions.AI.BackgroundResponsesOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" } ] }, @@ -997,16 +1035,24 @@ "Member": "bool? Microsoft.Extensions.AI.ChatOptions.AllowMultipleToolCalls { get; set; }", "Stage": "Stable" }, + { + "Member": "Microsoft.Extensions.AI.BackgroundResponsesOptions? Microsoft.Extensions.AI.ChatOptions.BackgroundResponsesOptions { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ResumptionToken? Microsoft.Extensions.AI.ChatOptions.ContinuationToken { get; set; }", + "Stage": "Experimental" + }, { "Member": "string? Microsoft.Extensions.AI.ChatOptions.ConversationId { get; set; }", "Stage": "Stable" }, { - "Member": "string? Microsoft.Extensions.AI.ChatOptions.Instructions { get; set; }", + "Member": "float? Microsoft.Extensions.AI.ChatOptions.FrequencyPenalty { get; set; }", "Stage": "Stable" }, { - "Member": "float? Microsoft.Extensions.AI.ChatOptions.FrequencyPenalty { get; set; }", + "Member": "string? Microsoft.Extensions.AI.ChatOptions.Instructions { get; set; }", "Stage": "Stable" }, { @@ -1089,6 +1135,10 @@ "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.ChatResponse.AdditionalProperties { get; set; }", "Stage": "Stable" }, + { + "Member": "Microsoft.Extensions.AI.ResumptionToken? Microsoft.Extensions.AI.ChatResponse.ContinuationToken { get; set; }", + "Stage": "Stable" + }, { "Member": "string? Microsoft.Extensions.AI.ChatResponse.ConversationId { get; set; }", "Stage": "Stable" @@ -1261,6 +1311,10 @@ "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.ChatResponseUpdate.Contents { get; set; }", "Stage": "Stable" }, + { + "Member": "Microsoft.Extensions.AI.ResumptionToken? Microsoft.Extensions.AI.ChatResponseUpdate.ContinuationToken { get; set; }", + "Stage": "Stable" + }, { "Member": "string? Microsoft.Extensions.AI.ChatResponseUpdate.ConversationId { get; set; }", "Stage": "Stable" @@ -1412,23 +1466,23 @@ ], "Properties": [ { - "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.Title { get; set; }", + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.FileId { get; set; }", "Stage": "Stable" }, { - "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.ToolName { get; set; }", + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.Snippet { get; set; }", "Stage": "Stable" }, { - "Member": "System.Uri? Microsoft.Extensions.AI.CitationAnnotation.Url { get; set; }", + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.Title { get; set; }", "Stage": "Stable" }, { - "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.FileId { get; set; }", + "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.ToolName { get; set; }", "Stage": "Stable" }, { - "Member": "string? Microsoft.Extensions.AI.CitationAnnotation.Snippet { get; set; }", + "Member": "System.Uri? Microsoft.Extensions.AI.CitationAnnotation.Url { get; set; }", "Stage": "Stable" } ] @@ -1464,11 +1518,11 @@ "Stage": "Stable" }, { - "Member": "string? Microsoft.Extensions.AI.DataContent.Name { get; set; }", + "Member": "string Microsoft.Extensions.AI.DataContent.MediaType { get; }", "Stage": "Stable" }, { - "Member": "string Microsoft.Extensions.AI.DataContent.MediaType { get; }", + "Member": "string? Microsoft.Extensions.AI.DataContent.Name { get; set; }", "Stage": "Stable" }, { @@ -1495,20 +1549,20 @@ }, { "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.ToString();", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { - "Member": "Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.DelegatingAIFunction.InnerFunction { get; }", + "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.DelegatingAIFunction.AdditionalProperties { get; }", "Stage": "Stable" }, { - "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.DelegatingAIFunction.AdditionalProperties { get; }", + "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Description { get; }", "Stage": "Stable" }, { - "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Description { get; }", + "Member": "Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.DelegatingAIFunction.InnerFunction { get; }", "Stage": "Stable" }, { @@ -1601,6 +1655,38 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.DelegatingImageGenerator : Microsoft.Extensions.AI.IImageGenerator, System.IDisposable", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.DelegatingImageGenerator.DelegatingImageGenerator(Microsoft.Extensions.AI.IImageGenerator innerGenerator);", + "Stage": "Experimental" + }, + { + "Member": "void Microsoft.Extensions.AI.DelegatingImageGenerator.Dispose();", + "Stage": "Experimental" + }, + { + "Member": "virtual void Microsoft.Extensions.AI.DelegatingImageGenerator.Dispose(bool disposing);", + "Stage": "Experimental" + }, + { + "Member": "virtual System.Threading.Tasks.Task Microsoft.Extensions.AI.DelegatingImageGenerator.GenerateAsync(Microsoft.Extensions.AI.ImageGenerationRequest request, Microsoft.Extensions.AI.ImageGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, + { + "Member": "virtual object? Microsoft.Extensions.AI.DelegatingImageGenerator.GetService(System.Type serviceType, object? serviceKey = null);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.IImageGenerator Microsoft.Extensions.AI.DelegatingImageGenerator.InnerGenerator { get; }", + "Stage": "Experimental" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.DelegatingSpeechToTextClient : Microsoft.Extensions.AI.ISpeechToTextClient, System.IDisposable", "Stage": "Experimental", @@ -1799,6 +1885,46 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string id, Microsoft.Extensions.AI.FunctionCallContent functionCall);", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.CreateResponse(bool approved);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionCall { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string id, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "bool Microsoft.Extensions.AI.FunctionApprovalResponseContent.Approved { get; }", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionCall { get; }", + "Stage": "Experimental" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.FunctionCallContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", @@ -1948,105 +2074,438 @@ ] }, { - "Type": "class Microsoft.Extensions.AI.HostedFileSearchTool : Microsoft.Extensions.AI.AITool", + "Type": "sealed class Microsoft.Extensions.AI.HostedFileContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.HostedFileSearchTool.HostedFileSearchTool();", + "Member": "Microsoft.Extensions.AI.HostedFileContent.HostedFileContent(string fileId);", "Stage": "Stable" } ], "Properties": [ { - "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedFileSearchTool.Inputs { get; set; }", - "Stage": "Stable" - }, - { - "Member": "int? Microsoft.Extensions.AI.HostedFileSearchTool.MaximumResultCount { get; set; }", - "Stage": "Stable" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.HostedWebSearchTool : Microsoft.Extensions.AI.AITool", - "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.HostedWebSearchTool.HostedWebSearchTool();", + "Member": "string Microsoft.Extensions.AI.HostedFileContent.FileId { get; set; }", "Stage": "Stable" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.HostedFileContent : Microsoft.Extensions.AI.AIContent", + "Type": "class Microsoft.Extensions.AI.HostedFileSearchTool : Microsoft.Extensions.AI.AITool", "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.HostedFileContent.HostedFileContent(string fileId);", + "Member": "Microsoft.Extensions.AI.HostedFileSearchTool.HostedFileSearchTool();", "Stage": "Stable" } ], "Properties": [ { - "Member": "string Microsoft.Extensions.AI.HostedFileContent.FileId { get; set; }", + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedFileSearchTool.Inputs { get; set; }", + "Stage": "Stable" + }, + { + "Member": "int? Microsoft.Extensions.AI.HostedFileSearchTool.MaximumResultCount { get; set; }", "Stage": "Stable" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.HostedVectorStoreContent : Microsoft.Extensions.AI.AIContent", - "Stage": "Stable", + "Type": "class Microsoft.Extensions.AI.HostedMcpServerTool : Microsoft.Extensions.AI.AITool", + "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.HostedVectorStoreContent.HostedVectorStoreContent(string vectorStoreId);", - "Stage": "Stable" + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string url);", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri url);", + "Stage": "Experimental" } ], "Properties": [ { - "Member": "string Microsoft.Extensions.AI.HostedVectorStoreContent.VectorStoreId { get; set; }", - "Stage": "Stable" - } - ] - }, - { - "Type": "interface Microsoft.Extensions.AI.IChatClient : System.IDisposable", - "Stage": "Stable", - "Methods": [ + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerTool.AllowedTools { get; set; }", + "Stage": "Experimental" + }, { - "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.IChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Stable" + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode? Microsoft.Extensions.AI.HostedMcpServerTool.ApprovalMode { get; set; }", + "Stage": "Experimental" }, { - "Member": "object? Microsoft.Extensions.AI.IChatClient.GetService(System.Type serviceType, object? serviceKey = null);", - "Stage": "Stable" + "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.HostedMcpServerTool.Headers { get; set; }", + "Stage": "Experimental" }, { - "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.IChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Stable" + "Member": "string? Microsoft.Extensions.AI.HostedMcpServerTool.ServerDescription { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerName { get; }", + "Stage": "Experimental" + }, + { + "Member": "System.Uri Microsoft.Extensions.AI.HostedMcpServerTool.Url { get; }", + "Stage": "Experimental" } ] }, { - "Type": "interface Microsoft.Extensions.AI.IEmbeddingGenerator : System.IDisposable", - "Stage": "Stable", + "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Experimental", "Methods": [ { - "Member": "object? Microsoft.Extensions.AI.IEmbeddingGenerator.GetService(System.Type serviceType, object? serviceKey = null);", - "Stage": "Stable" + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.HostedMcpServerToolAlwaysRequireApprovalMode();", + "Stage": "Experimental" + }, + { + "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.Equals(object? obj);", + "Stage": "Experimental" + }, + { + "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.GetHashCode();", + "Stage": "Experimental" } ] }, { - "Type": "interface Microsoft.Extensions.AI.IEmbeddingGenerator : Microsoft.Extensions.AI.IEmbeddingGenerator, System.IDisposable where TEmbedding : Microsoft.Extensions.AI.Embedding", - "Stage": "Stable", + "Type": "class Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Experimental", "Methods": [ { - "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IEmbeddingGenerator.GenerateAsync(System.Collections.Generic.IEnumerable values, Microsoft.Extensions.AI.EmbeddingGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.RequireSpecific(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.AlwaysRequire { get; }", + "Stage": "Experimental" + }, + { + "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.NeverRequire { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.HostedMcpServerToolNeverRequireApprovalMode();", + "Stage": "Experimental" + }, + { + "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.Equals(object? obj);", + "Stage": "Experimental" + }, + { + "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.GetHashCode();", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.HostedMcpServerToolRequireSpecificApprovalMode(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);", + "Stage": "Experimental" + }, + { + "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.Equals(object? obj);", + "Stage": "Experimental" + }, + { + "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.GetHashCode();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.AlwaysRequireApprovalToolNames { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.NeverRequireApprovalToolNames { get; set; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedVectorStoreContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedVectorStoreContent.HostedVectorStoreContent(string vectorStoreId);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.HostedVectorStoreContent.VectorStoreId { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.HostedWebSearchTool : Microsoft.Extensions.AI.AITool", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedWebSearchTool.HostedWebSearchTool();", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.IChatClient : System.IDisposable", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.IChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + }, + { + "Member": "object? Microsoft.Extensions.AI.IChatClient.GetService(System.Type serviceType, object? serviceKey = null);", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.IChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.IChatReducer", + "Stage": "Experimental", + "Methods": [ + { + "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IChatReducer.ReduceAsync(System.Collections.Generic.IEnumerable messages, System.Threading.CancellationToken cancellationToken);", + "Stage": "Experimental" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.IEmbeddingGenerator : System.IDisposable", + "Stage": "Stable", + "Methods": [ + { + "Member": "object? Microsoft.Extensions.AI.IEmbeddingGenerator.GetService(System.Type serviceType, object? serviceKey = null);", "Stage": "Stable" } ] }, + { + "Type": "interface Microsoft.Extensions.AI.IEmbeddingGenerator : Microsoft.Extensions.AI.IEmbeddingGenerator, System.IDisposable where TEmbedding : Microsoft.Extensions.AI.Embedding", + "Stage": "Stable", + "Methods": [ + { + "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IEmbeddingGenerator.GenerateAsync(System.Collections.Generic.IEnumerable values, Microsoft.Extensions.AI.EmbeddingGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.AI.IImageGenerator : System.IDisposable", + "Stage": "Experimental", + "Methods": [ + { + "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.IImageGenerator.GenerateAsync(Microsoft.Extensions.AI.ImageGenerationRequest request, Microsoft.Extensions.AI.ImageGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, + { + "Member": "object? Microsoft.Extensions.AI.IImageGenerator.GetService(System.Type serviceType, object? serviceKey = null);", + "Stage": "Experimental" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.ImageGenerationOptions", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ImageGenerationOptions.ImageGenerationOptions();", + "Stage": "Experimental" + }, + { + "Member": "virtual Microsoft.Extensions.AI.ImageGenerationOptions Microsoft.Extensions.AI.ImageGenerationOptions.Clone();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.ImageGenerationOptions.AdditionalProperties { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "int? Microsoft.Extensions.AI.ImageGenerationOptions.Count { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "System.Drawing.Size? Microsoft.Extensions.AI.ImageGenerationOptions.ImageSize { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "string? Microsoft.Extensions.AI.ImageGenerationOptions.MediaType { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "string? Microsoft.Extensions.AI.ImageGenerationOptions.ModelId { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "System.Func? Microsoft.Extensions.AI.ImageGenerationOptions.RawRepresentationFactory { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ImageGenerationResponseFormat? Microsoft.Extensions.AI.ImageGenerationOptions.ResponseFormat { get; set; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.ImageGenerationRequest", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ImageGenerationRequest.ImageGenerationRequest();", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ImageGenerationRequest.ImageGenerationRequest(string prompt);", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ImageGenerationRequest.ImageGenerationRequest(string prompt, System.Collections.Generic.IEnumerable? originalImages);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IEnumerable? Microsoft.Extensions.AI.ImageGenerationRequest.OriginalImages { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "string? Microsoft.Extensions.AI.ImageGenerationRequest.Prompt { get; set; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.ImageGenerationResponse", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ImageGenerationResponse.ImageGenerationResponse();", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ImageGenerationResponse.ImageGenerationResponse(System.Collections.Generic.IList? contents);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.ImageGenerationResponse.Contents { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "object? Microsoft.Extensions.AI.ImageGenerationResponse.RawRepresentation { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.UsageDetails? Microsoft.Extensions.AI.ImageGenerationResponse.Usage { get; set; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "enum Microsoft.Extensions.AI.ImageGenerationResponseFormat", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ImageGenerationResponseFormat.ImageGenerationResponseFormat();", + "Stage": "Experimental" + } + ], + "Fields": [ + { + "Member": "const Microsoft.Extensions.AI.ImageGenerationResponseFormat Microsoft.Extensions.AI.ImageGenerationResponseFormat.Data", + "Stage": "Experimental", + "Value": "1" + }, + { + "Member": "const Microsoft.Extensions.AI.ImageGenerationResponseFormat Microsoft.Extensions.AI.ImageGenerationResponseFormat.Hosted", + "Stage": "Experimental", + "Value": "2" + }, + { + "Member": "const Microsoft.Extensions.AI.ImageGenerationResponseFormat Microsoft.Extensions.AI.ImageGenerationResponseFormat.Uri", + "Stage": "Experimental", + "Value": "0" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.AI.ImageGeneratorExtensions", + "Stage": "Experimental", + "Methods": [ + { + "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.ImageGeneratorExtensions.EditImageAsync(this Microsoft.Extensions.AI.IImageGenerator generator, Microsoft.Extensions.AI.DataContent originalImage, string prompt, Microsoft.Extensions.AI.ImageGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, + { + "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.ImageGeneratorExtensions.EditImageAsync(this Microsoft.Extensions.AI.IImageGenerator generator, System.ReadOnlyMemory originalImageData, string fileName, string prompt, Microsoft.Extensions.AI.ImageGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, + { + "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.ImageGeneratorExtensions.EditImagesAsync(this Microsoft.Extensions.AI.IImageGenerator generator, System.Collections.Generic.IEnumerable originalImages, string prompt, Microsoft.Extensions.AI.ImageGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, + { + "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.ImageGeneratorExtensions.GenerateImagesAsync(this Microsoft.Extensions.AI.IImageGenerator generator, string prompt, Microsoft.Extensions.AI.ImageGenerationOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", + "Stage": "Experimental" + }, + { + "Member": "static object Microsoft.Extensions.AI.ImageGeneratorExtensions.GetRequiredService(this Microsoft.Extensions.AI.IImageGenerator generator, System.Type serviceType, object? serviceKey = null);", + "Stage": "Experimental" + }, + { + "Member": "static TService Microsoft.Extensions.AI.ImageGeneratorExtensions.GetRequiredService(this Microsoft.Extensions.AI.IImageGenerator generator, object? serviceKey = null);", + "Stage": "Experimental" + }, + { + "Member": "static TService? Microsoft.Extensions.AI.ImageGeneratorExtensions.GetService(this Microsoft.Extensions.AI.IImageGenerator generator, object? serviceKey = null);", + "Stage": "Experimental" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.ImageGeneratorMetadata", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ImageGeneratorMetadata.ImageGeneratorMetadata(string? providerName = null, System.Uri? providerUri = null, string? defaultModelId = null);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "string? Microsoft.Extensions.AI.ImageGeneratorMetadata.DefaultModelId { get; }", + "Stage": "Experimental" + }, + { + "Member": "string? Microsoft.Extensions.AI.ImageGeneratorMetadata.ProviderName { get; }", + "Stage": "Experimental" + }, + { + "Member": "System.Uri? Microsoft.Extensions.AI.ImageGeneratorMetadata.ProviderUri { get; }", + "Stage": "Experimental" + } + ] + }, { "Type": "interface Microsoft.Extensions.AI.ISpeechToTextClient : System.IDisposable", "Stage": "Experimental", @@ -2065,6 +2524,90 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.McpServerToolApprovalRequestContent(string id, Microsoft.Extensions.AI.McpServerToolCallContent toolCall);", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.McpServerToolApprovalResponseContent Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.CreateResponse(bool approved);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolCallContent Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.ToolCall { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolApprovalResponseContent.McpServerToolApprovalResponseContent(string id, bool approved);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "bool Microsoft.Extensions.AI.McpServerToolApprovalResponseContent.Approved { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string toolName, string serverName);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IReadOnlyDictionary? Microsoft.Extensions.AI.McpServerToolCallContent.Arguments { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.CallId { get; }", + "Stage": "Experimental" + }, + { + "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.ServerName { get; }", + "Stage": "Experimental" + }, + { + "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.ToolName { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.McpServerToolResultContent.CallId { get; }", + "Stage": "Experimental" + }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.McpServerToolResultContent.Output { get; set; }", + "Stage": "Experimental" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.NoneChatToolMode : Microsoft.Extensions.AI.ChatToolMode", "Stage": "Stable", @@ -2107,6 +2650,46 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.ResumptionToken", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ResumptionToken.ResumptionToken();", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ResumptionToken.ResumptionToken(byte[] bytes);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.ResumptionToken Microsoft.Extensions.AI.ResumptionToken.FromBytes(byte[] bytes);", + "Stage": "Stable" + }, + { + "Member": "virtual byte[] Microsoft.Extensions.AI.ResumptionToken.ToBytes();", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.ResumptionToken.Converter", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ResumptionToken.Converter.Converter();", + "Stage": "Stable" + }, + { + "Member": "override Microsoft.Extensions.AI.ResumptionToken Microsoft.Extensions.AI.ResumptionToken.Converter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options);", + "Stage": "Stable" + }, + { + "Member": "override void Microsoft.Extensions.AI.ResumptionToken.Converter.Write(System.Text.Json.Utf8JsonWriter writer, Microsoft.Extensions.AI.ResumptionToken value, System.Text.Json.JsonSerializerOptions options);", + "Stage": "Stable" + } + ] + }, { "Type": "static class Microsoft.Extensions.AI.SpeechToTextClientExtensions", "Stage": "Experimental", @@ -2246,6 +2829,10 @@ { "Member": "string Microsoft.Extensions.AI.SpeechToTextResponse.Text { get; }", "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.UsageDetails? Microsoft.Extensions.AI.SpeechToTextResponse.Usage { get; set; }", + "Stage": "Experimental" } ] }, @@ -2440,11 +3027,11 @@ ], "Properties": [ { - "Member": "string Microsoft.Extensions.AI.TextReasoningContent.Text { get; set; }", + "Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ProtectedData { get; set; }", "Stage": "Stable" }, { - "Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ProtectedData { get; set; }", + "Member": "string Microsoft.Extensions.AI.TextReasoningContent.Text { get; set; }", "Stage": "Stable" } ] @@ -2460,11 +3047,11 @@ ], "Properties": [ { - "Member": "int? Microsoft.Extensions.AI.TextSpanAnnotatedRegion.StartIndex { get; set; }", + "Member": "int? Microsoft.Extensions.AI.TextSpanAnnotatedRegion.EndIndex { get; set; }", "Stage": "Stable" }, { - "Member": "int? Microsoft.Extensions.AI.TextSpanAnnotatedRegion.EndIndex { get; set; }", + "Member": "int? Microsoft.Extensions.AI.TextSpanAnnotatedRegion.StartIndex { get; set; }", "Stage": "Stable" } ] @@ -2548,6 +3135,38 @@ "Stage": "Stable" } ] + }, + { + "Type": "class Microsoft.Extensions.AI.UserInputRequestContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.UserInputRequestContent.UserInputRequestContent(string id);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.UserInputRequestContent.Id { get; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.UserInputResponseContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.UserInputResponseContent.UserInputResponseContent(string id);", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.UserInputResponseContent.Id { get; }", + "Stage": "Experimental" + } + ] } ] } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ResumptionToken.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ResumptionToken.cs new file mode 100644 index 00000000000..2f8904af4b4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ResumptionToken.cs @@ -0,0 +1,67 @@ +// 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.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a token used to resume, continue, or rehydrate an operation across multiple scenarios/calls, +/// such as resuming a streamed response from a specific point or retrieving the result of a background operation. +/// Subclasses of this class encapsulate all necessary information within the token to facilitate these actions. +/// +[JsonConverter(typeof(Converter))] +public class ResumptionToken +{ + /// Bytes representing this token. + private readonly byte[]? _bytes; + + /// Initializes a new instance of the class. + protected ResumptionToken() + { + } + + /// Initializes a new instance of the class. + /// Bytes to create the token from. + protected ResumptionToken(byte[] bytes) + { + _ = Throw.IfNull(bytes); + + _bytes = bytes; + } + + /// Create a new instance of from the provided . + /// + /// Bytes obtained from calling on a . + /// A equivalent to the one from which + /// the original bytes were obtained. + public static ResumptionToken FromBytes(byte[] bytes) => new(bytes); + + /// Gets the bytes representing this . + /// The bytes of this . + public virtual byte[] ToBytes() => _bytes ?? throw new InvalidOperationException("Unable to obtain this token's bytes."); + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ResumptionToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return ResumptionToken.FromBytes(reader.GetBytesFromBase64()); + } + + /// + public override void Write(Utf8JsonWriter writer, ResumptionToken value, JsonSerializerOptions options) + { + _ = Throw.IfNull(writer); + _ = Throw.IfNull(value); + + writer.WriteBase64StringValue(value.ToBytes()); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index c632f3c45c7..0d119a742e2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -64,7 +64,7 @@ public static ChatResponse AsChatResponse(this OpenAIResponse response, Response /// is . public static IAsyncEnumerable AsChatResponseUpdatesAsync( this IAsyncEnumerable responseUpdates, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default) => - OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, cancellationToken); + OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, cancellationToken: cancellationToken); /// Creates an OpenAI from a . /// The response to convert. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 2720c7f761f..da55c8bc2d4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -85,12 +85,28 @@ public OpenAIResponsesChatClient(OpenAIResponseClient responseClient) public async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNull(messages); + var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? [.. messages]; - // Convert the inputs into what OpenAIResponseClient expects. - var openAIResponseItems = ToOpenAIResponseItems(messages, options); var openAIOptions = ToOpenAIResponseCreationOptions(options); + // Convert the inputs into what OpenAIResponseClient expects. + var openAIResponseItems = ToOpenAIResponseItems(inputMessages, options); + + // Provided continuation token signals that an existing background response should be fetched. + if (options?.ContinuationToken is { } token) + { + if (inputMessages.Count > 0) + { + throw new InvalidOperationException("Messages are not allowed when continuing a background response using a continuation token."); + } + + var continuationToken = OpenAIResponsesContinuationToken.FromToken(token); + + var response = await _responseClient.GetResponseAsync(continuationToken.ResponseId, cancellationToken).ConfigureAwait(false); + + return FromOpenAIResponse(response, openAIOptions); + } + // Make the call to the OpenAIResponseClient. var task = _createResponseAsync is not null ? _createResponseAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) : @@ -108,6 +124,7 @@ internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, R { ConversationId = openAIOptions?.StoredOutputEnabled is false ? null : openAIResponse.Id, CreatedAt = openAIResponse.CreatedAt, + ContinuationToken = GetContinuationToken(openAIResponse), FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason), ModelId = openAIResponse.Model, RawRepresentation = openAIResponse, @@ -219,28 +236,48 @@ internal static IEnumerable ToChatMessages(IEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { - _ = Throw.IfNull(messages); + var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? [.. messages]; - var openAIResponseItems = ToOpenAIResponseItems(messages, options); var openAIOptions = ToOpenAIResponseCreationOptions(options); + // Provided continuation token signals that an existing background response should be fetched. + if (options?.ContinuationToken is { } token) + { + if (inputMessages.Count > 0) + { + throw new InvalidOperationException("Messages are not allowed when resuming steamed background response using a continuation token."); + } + + var continuationToken = OpenAIResponsesContinuationToken.FromToken(token); + + IAsyncEnumerable updates = _responseClient.GetResponseStreamingAsync(continuationToken.ResponseId, continuationToken.SequenceNumber, cancellationToken); + + return FromOpenAIStreamingResponseUpdatesAsync(updates, openAIOptions, continuationToken.ResponseId, cancellationToken); + } + + var openAIResponseItems = ToOpenAIResponseItems(inputMessages, options); + var streamingUpdates = _createResponseStreamingAsync is not null ? _createResponseStreamingAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) : _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken); - return FromOpenAIStreamingResponseUpdatesAsync(streamingUpdates, openAIOptions, cancellationToken); + return FromOpenAIStreamingResponseUpdatesAsync(streamingUpdates, openAIOptions, cancellationToken: cancellationToken); } internal static async IAsyncEnumerable FromOpenAIStreamingResponseUpdatesAsync( - IAsyncEnumerable streamingResponseUpdates, ResponseCreationOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken = default) + IAsyncEnumerable streamingResponseUpdates, + ResponseCreationOptions? options, + string? resumeResponseId = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { DateTimeOffset? createdAt = null; - string? responseId = null; - string? conversationId = null; + string? responseId = resumeResponseId; + string? conversationId = options?.StoredOutputEnabled is false ? null : resumeResponseId; string? modelId = null; string? lastMessageId = null; ChatRole? lastRole = null; bool anyFunctions = false; + ResponseStatus? latestResponseStatus = null; await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false)) { @@ -254,6 +291,11 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => ModelId = modelId, RawRepresentation = streamingUpdate, ResponseId = responseId, + ContinuationToken = GetContinuationToken( + responseId!, + latestResponseStatus, + options?.BackgroundModeEnabled, + streamingUpdate.SequenceNumber) }; switch (streamingUpdate) @@ -263,10 +305,48 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => responseId = createdUpdate.Response.Id; conversationId = options?.StoredOutputEnabled is false ? null : responseId; modelId = createdUpdate.Response.Model; + latestResponseStatus = createdUpdate.Response.Status; + goto default; + + case StreamingResponseQueuedUpdate queuedUpdate: + createdAt = queuedUpdate.Response.CreatedAt; + responseId = queuedUpdate.Response.Id; + conversationId = options?.StoredOutputEnabled is false ? null : responseId; + modelId = queuedUpdate.Response.Model; + latestResponseStatus = queuedUpdate.Response.Status; + goto default; + + case StreamingResponseInProgressUpdate inProgressUpdate: + createdAt = inProgressUpdate.Response.CreatedAt; + responseId = inProgressUpdate.Response.Id; + conversationId = options?.StoredOutputEnabled is false ? null : responseId; + modelId = inProgressUpdate.Response.Model; + latestResponseStatus = inProgressUpdate.Response.Status; + goto default; + + case StreamingResponseIncompleteUpdate incompleteUpdate: + createdAt = incompleteUpdate.Response.CreatedAt; + responseId = incompleteUpdate.Response.Id; + conversationId = options?.StoredOutputEnabled is false ? null : responseId; + modelId = incompleteUpdate.Response.Model; + latestResponseStatus = incompleteUpdate.Response.Status; + goto default; + + case StreamingResponseFailedUpdate failedUpdate: + createdAt = failedUpdate.Response.CreatedAt; + responseId = failedUpdate.Response.Id; + conversationId = options?.StoredOutputEnabled is false ? null : responseId; + modelId = failedUpdate.Response.Model; + latestResponseStatus = failedUpdate.Response.Status; goto default; case StreamingResponseCompletedUpdate completedUpdate: { + createdAt = completedUpdate.Response.CreatedAt; + responseId = completedUpdate.Response.Id; + conversationId = options?.StoredOutputEnabled is false ? null : responseId; + modelId = completedUpdate.Response.Model; + latestResponseStatus = completedUpdate.Response?.Status; var update = CreateUpdate(ToUsageDetails(completedUpdate.Response) is { } usage ? new UsageContent(usage) : null); update.FinishReason = ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? @@ -416,6 +496,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt result.PreviousResponseId ??= options.ConversationId; result.Temperature ??= options.Temperature; result.TopP ??= options.TopP; + result.BackgroundModeEnabled ??= options.BackgroundResponsesOptions?.Allow; if (options.Instructions is { } instructions) { @@ -896,6 +977,51 @@ private static void AddAllMcpFilters(IList toolNames, McpToolFilter filt } } + private static OpenAIResponsesContinuationToken? GetContinuationToken(OpenAIResponse openAIResponse) + { + return GetContinuationToken( + responseId: openAIResponse.Id, + responseStatus: openAIResponse.Status, + isBackgroundModeEnabled: openAIResponse.BackgroundModeEnabled); + } + + private static OpenAIResponsesContinuationToken? GetContinuationToken( + string responseId, + ResponseStatus? responseStatus, + bool? isBackgroundModeEnabled, + int? updateSequenceNumber = null) + { + if (isBackgroundModeEnabled is not true) + { + return null; + } + + // Return a continuation token for in-progress or queued responses because they are not yet complete. + if (responseStatus is (ResponseStatus.InProgress or ResponseStatus.Queued)) + { + return new OpenAIResponsesContinuationToken(responseId) + { + SequenceNumber = updateSequenceNumber, + }; + } + else if (responseStatus is null && updateSequenceNumber is not null) + { + // In some cases, streaming needs to be resumed from an event (e.g., a text delta event) that does not have a response status, + // response Id, and potentially other properties. In these cases, we know that the response is not yet complete + // because we are receiving updates for it, so we create a continuation token from the response Id obtained from the continuation token + // supplied to resume the streaming and the sequence number available on the event. + return new OpenAIResponsesContinuationToken(responseId) + { + SequenceNumber = updateSequenceNumber, + }; + } + + // For all other statuses: completed, failed, canceled, incomplete + // return null to indicate the operation is finished allowing the caller + // to stop and access the final result, failure details, reason for incompletion, etc. + return null; + } + /// Provides an wrapper for a . internal sealed class ResponseToolAITool(ResponseTool tool) : AITool { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs new file mode 100644 index 00000000000..fc82abd64ee --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs @@ -0,0 +1,106 @@ +// 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.IO; +using System.Text.Json; + +namespace Microsoft.Extensions.AI; + +/// Represents a continuation token for OpenAI responses. +/// +/// The token is used for resuming streamed background responses and continuing +/// non-streamed background responses until completion. +/// +internal sealed class OpenAIResponsesContinuationToken : ResumptionToken +{ + /// Initializes a new instance of the class. + internal OpenAIResponsesContinuationToken(string responseId) + { + ResponseId = responseId; + } + + /// Gets or sets the Id of the response. + internal string ResponseId { get; set; } + + /// Gets or sets the sequence number of a streamed update. + internal int? SequenceNumber { get; set; } + + /// + public override byte[] ToBytes() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + writer.WriteStartObject(); + + writer.WriteString("responseId", ResponseId); + + if (SequenceNumber.HasValue) + { + writer.WriteNumber("sequenceNumber", SequenceNumber.Value); + } + + writer.WriteEndObject(); + + writer.Flush(); + stream.Position = 0; + + return stream.ToArray(); + } + + /// Create a new instance of from the provided . + /// + /// The token to create the from. + /// A equivalent of the provided . + internal static OpenAIResponsesContinuationToken FromToken(ResumptionToken token) + { + if (token is OpenAIResponsesContinuationToken longRunResumptionToken) + { + return longRunResumptionToken; + } + + byte[] data = token.ToBytes(); + + if (data.Length == 0) + { + throw new InvalidOperationException("Failed to create OpenAIResponsesResumptionToken from provided token because it does not contain any data."); + } + + Utf8JsonReader reader = new(data); + + string responseId = null!; + int? startAfter = null; + + _ = reader.Read(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string propertyName = reader.GetString()!; + + switch (propertyName) + { + case "responseId": + _ = reader.Read(); + responseId = reader.GetString()!; + break; + case "sequenceNumber": + _ = reader.Read(); + startAfter = reader.GetInt32(); + break; + default: + throw new JsonException($"Unrecognized property '{propertyName}'."); + } + } + + return new(responseId) + { + SequenceNumber = startAfter + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 0137ac06c7f..9b148fa5031 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -812,6 +812,19 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions? options, stri options = options.Clone(); options.ConversationId = conversationId; } + else if (options.ContinuationToken is not null) + { + // Clone options before resetting the continuation token below. + options = options.Clone(); + } + + // Reset the continuation token of a background response operation + // to signal the inner client to handle function call result rather + // than getting the result of the operation. + if (options?.ContinuationToken is not null) + { + options.ContinuationToken = null; + } } /// Gets whether the function calling loop should exit based on the function call requests. diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 0d77680cbc4..a9a73ab262f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -14,7 +14,7 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA2227;CA1034;SA1316;S1067;S1121;S1994;S3253;MEAI001 + $(NoWarn);CA2227;CA1034;SA1316;S1067;S1121;S1994;S3253;MEAI001;S104