mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add support for background responses (#1501)
* add support for background responses * Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix broken link * fix xml comments and background responses properties override funcitonity * change ai model provider * use Run{Streaming}Async overloads that don't require messages * stop using m: prefix in cref attribute of <see/> element. * reject input messages provided with continuation token + don't extract messages from message store and context provide if continuation token is provided * use agent thread for background-responses sample * require agent thread for background responses * Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> * remove CA1200 * Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * address pr review comments * Update dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
Roger Barreto
westey
parent
699149c260
commit
1bf520a7c2
@@ -10,9 +10,6 @@ namespace Microsoft.Agents.AI;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class currently has no options, but may be extended in the future to include additional configuration settings.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations of <see cref="AIAgent"/> may provide subclasses of <see cref="AgentRunOptions"/> with additional options specific to that agent type.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
@@ -33,5 +30,48 @@ public class AgentRunOptions
|
||||
public AgentRunOptions(AgentRunOptions options)
|
||||
{
|
||||
_ = Throw.IfNull(options);
|
||||
this.ContinuationToken = options.ContinuationToken;
|
||||
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the continuation token for resuming and getting the result of the agent response identified by this token.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is used for background responses that can be activated via the <see cref="AllowBackgroundResponses"/>
|
||||
/// property if the <see cref="AIAgent"/> implementation supports them.
|
||||
/// Streamed background responses, such as those returned by default by <see cref="AIAgent.RunStreamingAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>
|
||||
/// can be resumed if interrupted. This means that a continuation token obtained from the <see cref="AgentRunResponseUpdate.ContinuationToken"/>
|
||||
/// 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 <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>,
|
||||
/// can be polled for completion by obtaining the token from the <see cref="AgentRunResponse.ContinuationToken"/> property
|
||||
/// and passing it via this property on subsequent calls to <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>.
|
||||
/// </remarks>
|
||||
public object? ContinuationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the background responses are allowed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Background responses allow running long-running operations or tasks asynchronously in the background that can be resumed by streaming APIs
|
||||
/// and polled for completion by non-streaming APIs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When this property is set to true, non-streaming APIs may start a background operation and return an initial
|
||||
/// response with a continuation token. Subsequent calls to the same API should be made in a polling manner with
|
||||
/// the continuation token to get the final result of the operation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When this property is set to true, streaming APIs may also start a background operation and begin streaming
|
||||
/// response updates until the operation is completed. If the streaming connection is interrupted, the
|
||||
/// continuation token obtained from the last update that has one should be supplied to a subsequent call to the same streaming API
|
||||
/// to resume the stream from the point of interruption and continue receiving updates until the operation is completed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property only takes effect if the implementation it's used with supports background responses.
|
||||
/// If the implementation does not support background responses, this property will be ignored.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool? AllowBackgroundResponses { get; set; }
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ public class AgentRunResponse
|
||||
this.RawRepresentation = response;
|
||||
this.ResponseId = response.ResponseId;
|
||||
this.Usage = response.Usage;
|
||||
this.ContinuationToken = response.ContinuationToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -159,6 +160,23 @@ public class AgentRunResponse
|
||||
/// </value>
|
||||
public string? ResponseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the continuation token for getting the result of a background agent response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AIAgent"/> implementations that support background responses will return
|
||||
/// a continuation token if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
|
||||
/// and the result of the response has not been obtained yet. If the response has completed and the result has been obtained,
|
||||
/// the token will be <see langword="null"/>.
|
||||
/// <para>
|
||||
/// This property should be used in conjunction with <see cref="AgentRunOptions.ContinuationToken"/> to
|
||||
/// continue to poll for the completion of the response. Pass this token to
|
||||
/// <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>
|
||||
/// to poll for completion.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public object? ContinuationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp indicating when this response was created.
|
||||
/// </summary>
|
||||
@@ -234,7 +252,7 @@ public class AgentRunResponse
|
||||
{
|
||||
extra = new AgentRunResponseUpdate
|
||||
{
|
||||
AdditionalProperties = this.AdditionalProperties
|
||||
AdditionalProperties = this.AdditionalProperties,
|
||||
};
|
||||
|
||||
if (this.Usage is { } usage)
|
||||
|
||||
@@ -42,6 +42,7 @@ public static class AgentRunResponseExtensions
|
||||
RawRepresentation = response,
|
||||
ResponseId = response.ResponseId,
|
||||
Usage = response.Usage,
|
||||
ContinuationToken = response.ContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,6 +75,7 @@ public static class AgentRunResponseExtensions
|
||||
RawRepresentation = responseUpdate,
|
||||
ResponseId = responseUpdate.ResponseId,
|
||||
Role = responseUpdate.Role,
|
||||
ContinuationToken = responseUpdate.ContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ public class AgentRunResponseUpdate
|
||||
this.RawRepresentation = chatResponseUpdate;
|
||||
this.ResponseId = chatResponseUpdate.ResponseId;
|
||||
this.Role = chatResponseUpdate.Role;
|
||||
this.ContinuationToken = chatResponseUpdate.ContinuationToken;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the name of the author of the response update.</summary>
|
||||
@@ -148,6 +149,21 @@ public class AgentRunResponseUpdate
|
||||
/// <summary>Gets or sets a timestamp for the response update.</summary>
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the continuation token for resuming the streamed agent response of which this update is a part.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AIAgent"/> implementations that support background responses will return
|
||||
/// a continuation token on each update if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
|
||||
/// except for the last update, for which the token will be <see langword="null"/>.
|
||||
/// <para>
|
||||
/// This property should be used for stream resumption, where the continuation token of the latest received update should be
|
||||
/// passed to <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunStreamingAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>
|
||||
/// to resume streaming from the point of interruption.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public object? ContinuationToken { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.Text;
|
||||
|
||||
|
||||
@@ -436,13 +436,13 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
// If no agent chat options were provided, return the request chat options as is.
|
||||
if (this._agentOptions?.ChatOptions is null)
|
||||
{
|
||||
return requestChatOptions;
|
||||
return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
|
||||
}
|
||||
|
||||
// If no request chat options were provided, use the agent's chat options clone.
|
||||
if (requestChatOptions is null)
|
||||
{
|
||||
return this._agentOptions?.ChatOptions.Clone();
|
||||
return ApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions);
|
||||
}
|
||||
|
||||
// If both are present, we need to merge them.
|
||||
@@ -532,7 +532,20 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
return requestChatOptions;
|
||||
return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
|
||||
|
||||
static ChatOptions? ApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
|
||||
{
|
||||
// If any of the background response properties are set in the run options, we should apply both to the chat options.
|
||||
if (agentRunOptions?.AllowBackgroundResponses is not null || agentRunOptions?.ContinuationToken is not null)
|
||||
{
|
||||
chatOptions ??= new ChatOptions();
|
||||
chatOptions.AllowBackgroundResponses = agentRunOptions.AllowBackgroundResponses;
|
||||
chatOptions.ContinuationToken = agentRunOptions.ContinuationToken;
|
||||
}
|
||||
|
||||
return chatOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -551,50 +564,67 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
{
|
||||
ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions);
|
||||
|
||||
// Supplying a thread for background responses is required to prevent inconsistent experience
|
||||
// for callers if they forget to provide the thread for initial or follow-up runs.
|
||||
if (chatOptions?.AllowBackgroundResponses is true && thread is null)
|
||||
{
|
||||
throw new InvalidOperationException("A thread must be provided when continuing a background response with a continuation token.");
|
||||
}
|
||||
|
||||
thread ??= this.GetNewThread();
|
||||
if (thread is not ChatClientAgentThread typedThread)
|
||||
{
|
||||
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
|
||||
}
|
||||
|
||||
// Add any existing messages from the thread to the messages to be sent to the chat client.
|
||||
List<ChatMessage> threadMessages = [];
|
||||
if (typedThread.MessageStore is not null)
|
||||
// Supplying messages when continuing a background response is not allowed.
|
||||
if (chatOptions?.ContinuationToken is not null && inputMessages.Any())
|
||||
{
|
||||
threadMessages.AddRange(await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
|
||||
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
|
||||
}
|
||||
List<ChatMessage> threadMessages = [];
|
||||
|
||||
// If we have an AIContextProvider, we should get context from it, and update our
|
||||
// messages and options with the additional context.
|
||||
if (typedThread.AIContextProvider is not null)
|
||||
// Populate the thread messages only if we are not continuing an existing response as it's not allowed
|
||||
if (chatOptions?.ContinuationToken is null)
|
||||
{
|
||||
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
|
||||
var aiContext = await typedThread.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
|
||||
if (aiContext.Messages is { Count: > 0 })
|
||||
// Add any existing messages from the thread to the messages to be sent to the chat client.
|
||||
if (typedThread.MessageStore is not null)
|
||||
{
|
||||
threadMessages.AddRange(aiContext.Messages);
|
||||
threadMessages.AddRange(await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (aiContext.Tools is { Count: > 0 })
|
||||
// If we have an AIContextProvider, we should get context from it, and update our
|
||||
// messages and options with the additional context.
|
||||
if (typedThread.AIContextProvider is not null)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Tools ??= [];
|
||||
foreach (AITool tool in aiContext.Tools)
|
||||
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
|
||||
var aiContext = await typedThread.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
|
||||
if (aiContext.Messages is { Count: > 0 })
|
||||
{
|
||||
chatOptions.Tools.Add(tool);
|
||||
threadMessages.AddRange(aiContext.Messages);
|
||||
}
|
||||
|
||||
if (aiContext.Tools is { Count: > 0 })
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Tools ??= [];
|
||||
foreach (AITool tool in aiContext.Tools)
|
||||
{
|
||||
chatOptions.Tools.Add(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if (aiContext.Instructions is not null)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
|
||||
}
|
||||
}
|
||||
|
||||
if (aiContext.Instructions is not null)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
|
||||
}
|
||||
// Add the input messages to the end of thread messages.
|
||||
threadMessages.AddRange(inputMessages);
|
||||
}
|
||||
|
||||
// Add the input messages to the end of thread messages.
|
||||
threadMessages.AddRange(inputMessages);
|
||||
|
||||
// If a user provided two different thread ids, via the thread object and options, we should throw
|
||||
// since we don't know which one to use.
|
||||
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && typedThread.ConversationId != chatOptions!.ConversationId)
|
||||
|
||||
Reference in New Issue
Block a user