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:
committed by
GitHub
Unverified
parent
699149c260
commit
1bf520a7c2
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@ Sub-packages are comprised of two parts, the code itself and the dependencies, t
|
||||
- Subpackage naming should also follow this, so in principle a package name is `<vendor/folder>-<feature/brand>`, so `google-gemini`, `azure-purview`, `microsoft-copilotstudio`, etc. For smaller vendors, where it's less likely to have a multitude of connectors, we can skip the feature/brand part, so `mem0`, `redis`, etc.
|
||||
- For Microsoft services we will have two vendor folders, `azure` and `microsoft`, where `azure` contains all Azure services, while `microsoft` contains other Microsoft services, such as Copilot Studio Agents.
|
||||
|
||||
This setup was discussed at length and the decision is captured in [ADR-0007](../decisions/0007-python-subpackages.md).
|
||||
This setup was discussed at length and the decision is captured in [ADR-0008](../decisions/0008-python-subpackages.md).
|
||||
|
||||
#### Evolving the package structure
|
||||
For each of the advanced components, we have two reason why we may split them into a folder, with an `__init__.py` and optionally a `_files.py`:
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step14_Middleware/Agent_Step14_Middleware.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step15_Plugins/Agent_Step15_Plugins.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
|
||||
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
|
||||
@@ -162,8 +163,14 @@
|
||||
<Folder Name="/Solution Items/docs/" />
|
||||
<Folder Name="/Solution Items/docs/decisions/">
|
||||
<File Path="../docs/decisions/0001-agent-run-response.md" />
|
||||
<File Path="../docs/decisions/0001-agent-tools.md" />
|
||||
<File Path="../docs/decisions/0002-agent-opentelemetry-instrumentation.md" />
|
||||
<File Path="../docs/decisions/0002-agent-tools.md" />
|
||||
<File Path="../docs/decisions/0003-agent-opentelemetry-instrumentation.md" />
|
||||
<File Path="../docs/decisions/0004-foundry-sdk-extensions.md" />
|
||||
<File Path="../docs/decisions/0005-python-naming-conventions.md" />
|
||||
<File Path="../docs/decisions/0006-userapproval.md" />
|
||||
<File Path="../docs/decisions/0007-agent-filtering-middleware.md" />
|
||||
<File Path="../docs/decisions/0008-python-subpackages.md" />
|
||||
<File Path="../docs/decisions/0009-support-long-running-operations.md" />
|
||||
<File Path="../docs/decisions/adr-short-template.md" />
|
||||
<File Path="../docs/decisions/adr-template.md" />
|
||||
<File Path="../docs/decisions/README.md" />
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use background responses with ChatClientAgent and OpenAI Responses.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Enable background responses (only supported by OpenAI Responses at this time).
|
||||
AgentRunOptions options = new() { AllowBackgroundResponses = true };
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
// Start the initial run.
|
||||
AgentRunResponse response = await agent.RunAsync("Tell me a joke about a pirate.", thread, options);
|
||||
|
||||
// Poll until the response is complete.
|
||||
while (response.ContinuationToken is { } token)
|
||||
{
|
||||
// Wait before polling again.
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
|
||||
// Continue with the token.
|
||||
options.ContinuationToken = token;
|
||||
|
||||
response = await agent.RunAsync(thread, options);
|
||||
}
|
||||
|
||||
// Display the result.
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
// Reset options and thread for streaming.
|
||||
options = new() { AllowBackgroundResponses = true };
|
||||
thread = agent.GetNewThread();
|
||||
|
||||
AgentRunResponseUpdate? lastReceivedUpdate = null;
|
||||
// Start streaming.
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate.", thread, options))
|
||||
{
|
||||
// Output each update.
|
||||
Console.Write(update.Text);
|
||||
|
||||
// Track last update.
|
||||
lastReceivedUpdate = update;
|
||||
|
||||
// Simulate connection loss after first piece of content received.
|
||||
if (update.Text.Length > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resume from interruption point.
|
||||
options.ContinuationToken = lastReceivedUpdate?.ContinuationToken;
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(thread, options))
|
||||
{
|
||||
// Output each update.
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# What This Sample Shows
|
||||
|
||||
This sample demonstrates how to use background responses with ChatCompletionAgent and OpenAI Responses for long-running operations. Background responses support:
|
||||
|
||||
- **Polling for completion** - Non-streaming APIs can start a background operation and return a continuation token. Poll with the token until the response completes.
|
||||
- **Resuming after interruption** - Streaming APIs can be interrupted and resumed from the last update using the continuation token.
|
||||
|
||||
> **Note:** Background responses are currently only supported by OpenAI Responses.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 8.0 SDK or later
|
||||
- OpenAI api key
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
@@ -42,6 +42,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[Using middleware with an agent](./Agent_Step14_Middleware/)|This sample demonstrates how to use middleware with an agent|
|
||||
|[Using plugins with an agent](./Agent_Step15_Plugins/)|This sample demonstrates how to use plugins with an agent|
|
||||
|[Reducing chat history size](./Agent_Step16_ChatReduction/)|This sample demonstrates how to reduce the chat history to constrain its size, where chat history is maintained locally|
|
||||
|[Background responses](./Agent_Step17_BackgroundResponses/)|This sample demonstrates how to use background responses for long-running operations with polling and resumption support|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
@@ -13,15 +15,44 @@ public class AgentRunOptionsTests
|
||||
public void CloningConstructorCopiesProperties()
|
||||
{
|
||||
// Arrange
|
||||
var options = new AgentRunOptions();
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ContinuationToken = new object(),
|
||||
AllowBackgroundResponses = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var clone = new AgentRunOptions(options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(clone);
|
||||
Assert.Same(options.ContinuationToken, clone.ContinuationToken);
|
||||
Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloningConstructorThrowsIfNull() =>
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new AgentRunOptions(null!));
|
||||
|
||||
[Fact]
|
||||
public void JsonSerializationRoundtrips()
|
||||
{
|
||||
// Arrange
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
||||
AllowBackgroundResponses = true
|
||||
};
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(options, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
var deserialized = JsonSerializer.Deserialize<AgentRunOptions>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), deserialized!.ContinuationToken);
|
||||
Assert.Equal(options.AllowBackgroundResponses, deserialized.AllowBackgroundResponses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ public class AgentRunResponseTests
|
||||
response = new();
|
||||
Assert.Empty(response.Messages);
|
||||
Assert.Empty(response.Text);
|
||||
Assert.Null(response.ContinuationToken);
|
||||
|
||||
response = new((IList<ChatMessage>?)null);
|
||||
Assert.Empty(response.Messages);
|
||||
Assert.Empty(response.Text);
|
||||
Assert.Null(response.ContinuationToken);
|
||||
|
||||
Assert.Throws<ArgumentNullException>("message", () => new AgentRunResponse((ChatMessage)null!));
|
||||
}
|
||||
@@ -55,6 +57,7 @@ public class AgentRunResponseTests
|
||||
RawRepresentation = new object(),
|
||||
ResponseId = "responseId",
|
||||
Usage = new UsageDetails(),
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
||||
};
|
||||
|
||||
AgentRunResponse response = new(chatResponse);
|
||||
@@ -64,6 +67,7 @@ public class AgentRunResponseTests
|
||||
Assert.Equal(chatResponse.ResponseId, response.ResponseId);
|
||||
Assert.Same(chatResponse, response.RawRepresentation as ChatResponse);
|
||||
Assert.Same(chatResponse.Usage, response.Usage);
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), response.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -97,6 +101,10 @@ public class AgentRunResponseTests
|
||||
AdditionalPropertiesDictionary additionalProps = [];
|
||||
response.AdditionalProperties = additionalProps;
|
||||
Assert.Same(additionalProps, response.AdditionalProperties);
|
||||
|
||||
Assert.Null(response.ContinuationToken);
|
||||
response.ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), response.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -110,11 +118,12 @@ public class AgentRunResponseTests
|
||||
Usage = new UsageDetails(),
|
||||
RawRepresentation = new(),
|
||||
AdditionalProperties = new() { ["key"] = "value" },
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.AgentRunResponse);
|
||||
string json = JsonSerializer.Serialize(original, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
AgentRunResponse? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.AgentRunResponse);
|
||||
AgentRunResponse? result = JsonSerializer.Deserialize<AgentRunResponse>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ChatRole.Assistant, result.Messages.Single().Role);
|
||||
@@ -130,6 +139,7 @@ public class AgentRunResponseTests
|
||||
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
|
||||
Assert.IsType<JsonElement>(value);
|
||||
Assert.Equal("value", ((JsonElement)value!).GetString());
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), result.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+13
-2
@@ -23,6 +23,7 @@ public class AgentRunResponseUpdateTests
|
||||
Assert.Null(update.MessageId);
|
||||
Assert.Null(update.CreatedAt);
|
||||
Assert.Equal(string.Empty, update.ToString());
|
||||
Assert.Null(update.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -41,6 +42,7 @@ public class AgentRunResponseUpdateTests
|
||||
RawRepresentation = new object(),
|
||||
ResponseId = "responseId",
|
||||
Role = ChatRole.Assistant,
|
||||
ContinuationToken = new object(),
|
||||
};
|
||||
|
||||
AgentRunResponseUpdate response = new(chatResponseUpdate);
|
||||
@@ -52,6 +54,7 @@ public class AgentRunResponseUpdateTests
|
||||
Assert.Same(chatResponseUpdate, response.RawRepresentation as ChatResponseUpdate);
|
||||
Assert.Equal(chatResponseUpdate.ResponseId, response.ResponseId);
|
||||
Assert.Equal(chatResponseUpdate.Role, response.Role);
|
||||
Assert.Same(chatResponseUpdate.ContinuationToken, response.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -102,6 +105,10 @@ public class AgentRunResponseUpdateTests
|
||||
Assert.Null(update.CreatedAt);
|
||||
update.CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), update.CreatedAt);
|
||||
|
||||
Assert.Null(update.ContinuationToken);
|
||||
update.ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), update.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -152,11 +159,12 @@ public class AgentRunResponseUpdateTests
|
||||
MessageId = "messageid",
|
||||
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
AdditionalProperties = new() { ["key"] = "value" },
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.AgentRunResponseUpdate);
|
||||
string json = JsonSerializer.Serialize(original, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
AgentRunResponseUpdate? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.AgentRunResponseUpdate);
|
||||
AgentRunResponseUpdate? result = JsonSerializer.Deserialize<AgentRunResponseUpdate>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(5, result.Contents.Count);
|
||||
@@ -187,5 +195,8 @@ public class AgentRunResponseUpdateTests
|
||||
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
|
||||
Assert.IsType<JsonElement>(value);
|
||||
Assert.Equal("value", ((JsonElement)value!).GetString());
|
||||
|
||||
Assert.NotNull(result.ContinuationToken);
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), result.ContinuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1951,6 +1951,499 @@ public partial class ChatClientAgentTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Background Responses Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task RunAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
|
||||
{
|
||||
// Arrange
|
||||
object continuationToken = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null });
|
||||
|
||||
AgentRunOptions agentRunOptions;
|
||||
|
||||
if (providePropsViaChatOptions)
|
||||
{
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
AllowBackgroundResponses = true,
|
||||
ContinuationToken = continuationToken
|
||||
};
|
||||
|
||||
agentRunOptions = new ChatClientAgentRunOptions(chatOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
agentRunOptions = new AgentRunOptions()
|
||||
{
|
||||
AllowBackgroundResponses = true,
|
||||
ContinuationToken = continuationToken
|
||||
};
|
||||
}
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
ChatClientAgentThread thread = new();
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(thread, options: agentRunOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.True(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
object continuationToken1 = new();
|
||||
object continuationToken2 = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null });
|
||||
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
AllowBackgroundResponses = true,
|
||||
ContinuationToken = continuationToken1
|
||||
};
|
||||
|
||||
ChatClientAgentRunOptions agentRunOptions = new(chatOptions)
|
||||
{
|
||||
AllowBackgroundResponses = false,
|
||||
ContinuationToken = continuationToken2
|
||||
};
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(options: agentRunOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.False(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task RunStreamingAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh"),
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "at?"),
|
||||
];
|
||||
|
||||
object continuationToken = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
|
||||
.Returns(ToAsyncEnumerableAsync(returnUpdates));
|
||||
|
||||
AgentRunOptions agentRunOptions;
|
||||
|
||||
if (providePropsViaChatOptions)
|
||||
{
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
AllowBackgroundResponses = true,
|
||||
ContinuationToken = continuationToken
|
||||
};
|
||||
|
||||
agentRunOptions = new ChatClientAgentRunOptions(chatOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
agentRunOptions = new AgentRunOptions()
|
||||
{
|
||||
AllowBackgroundResponses = true,
|
||||
ContinuationToken = continuationToken
|
||||
};
|
||||
}
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
ChatClientAgentThread thread = new();
|
||||
|
||||
// Act
|
||||
await foreach (var _ in agent.RunStreamingAsync(thread, options: agentRunOptions))
|
||||
{
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
|
||||
Assert.True(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh"),
|
||||
];
|
||||
|
||||
object continuationToken1 = new();
|
||||
object continuationToken2 = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
|
||||
.Returns(ToAsyncEnumerableAsync(returnUpdates));
|
||||
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
AllowBackgroundResponses = true,
|
||||
ContinuationToken = continuationToken1
|
||||
};
|
||||
|
||||
ChatClientAgentRunOptions agentRunOptions = new(chatOptions)
|
||||
{
|
||||
AllowBackgroundResponses = false,
|
||||
ContinuationToken = continuationToken2
|
||||
};
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Act
|
||||
await foreach (var _ in agent.RunStreamingAsync(options: agentRunOptions))
|
||||
{
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.False(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncPropagatesContinuationTokenFromChatResponseToAgentRunResponseAsync()
|
||||
{
|
||||
// Arrange
|
||||
object continuationToken = new();
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "partial")]) { ContinuationToken = continuationToken });
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
var runOptions = new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true });
|
||||
|
||||
ChatClientAgentThread thread = new();
|
||||
|
||||
// Act
|
||||
var response = await agent.RunAsync([new(ChatRole.User, "hi")], thread, options: runOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Same(continuationToken, response.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync()
|
||||
{
|
||||
// Arrange
|
||||
object token1 = new();
|
||||
ChatResponseUpdate[] expectedUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "pa") { ContinuationToken = token1 },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "rt") { ContinuationToken = null } // terminal
|
||||
];
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(expectedUpdates));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
ChatClientAgentThread thread = new();
|
||||
|
||||
// Act
|
||||
var actualUpdates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var u in agent.RunStreamingAsync([new(ChatRole.User, "hi")], thread, options: new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true })))
|
||||
{
|
||||
actualUpdates.Add(u);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, actualUpdates.Count);
|
||||
Assert.Same(token1, actualUpdates[0].ContinuationToken);
|
||||
Assert.Null(actualUpdates[1].ContinuationToken); // last update has null token
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = new() };
|
||||
|
||||
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(inputMessages, options: runOptions));
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = new() };
|
||||
|
||||
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions))
|
||||
{
|
||||
// Should not reach here
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> capturedMessages = [];
|
||||
|
||||
// Create a mock message store that would normally provide messages
|
||||
var mockMessageStore = new Mock<ChatMessageStore>();
|
||||
mockMessageStore
|
||||
.Setup(ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
|
||||
|
||||
// Create a mock AI context provider that would normally provide context
|
||||
var mockContextProvider = new Mock<AIContextProvider>();
|
||||
mockContextProvider
|
||||
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AIContext
|
||||
{
|
||||
Messages = [new(ChatRole.System, "Message from AI context")],
|
||||
Instructions = "context instructions"
|
||||
});
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedMessages.AddRange(msgs))
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "continued response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Create a thread with both message store and AI context provider
|
||||
ChatClientAgentThread thread = new()
|
||||
{
|
||||
MessageStore = mockMessageStore.Object,
|
||||
AIContextProvider = mockContextProvider.Object
|
||||
};
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = new() };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync([], thread, options: runOptions);
|
||||
|
||||
// Assert
|
||||
|
||||
// With continuation token, thread message population should be skipped
|
||||
Assert.Empty(capturedMessages);
|
||||
|
||||
// Verify that message store was never called due to continuation token
|
||||
mockMessageStore.Verify(
|
||||
ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
|
||||
// Verify that AI context provider was never called due to continuation token
|
||||
mockContextProvider.Verify(
|
||||
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> capturedMessages = [];
|
||||
|
||||
// Create a mock message store that would normally provide messages
|
||||
var mockMessageStore = new Mock<ChatMessageStore>();
|
||||
mockMessageStore
|
||||
.Setup(ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
|
||||
|
||||
// Create a mock AI context provider that would normally provide context
|
||||
var mockContextProvider = new Mock<AIContextProvider>();
|
||||
mockContextProvider
|
||||
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AIContext
|
||||
{
|
||||
Messages = [new(ChatRole.System, "Message from AI context")],
|
||||
Instructions = "context instructions"
|
||||
});
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedMessages.AddRange(msgs))
|
||||
.Returns(ToAsyncEnumerableAsync([new ChatResponseUpdate(role: ChatRole.Assistant, content: "continued response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Create a thread with both message store and AI context provider
|
||||
ChatClientAgentThread thread = new()
|
||||
{
|
||||
MessageStore = mockMessageStore.Object,
|
||||
AIContextProvider = mockContextProvider.Object
|
||||
};
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = new() };
|
||||
|
||||
// Act
|
||||
await agent.RunStreamingAsync([], thread, options: runOptions).ToListAsync();
|
||||
|
||||
// Assert
|
||||
|
||||
// With continuation token, thread message population should be skipped
|
||||
Assert.Empty(capturedMessages);
|
||||
|
||||
// Verify that message store was never called due to continuation token
|
||||
mockMessageStore.Verify(
|
||||
ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
|
||||
// Verify that AI context provider was never called due to continuation token
|
||||
mockContextProvider.Verify(
|
||||
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
AgentRunOptions runOptions = new() { AllowBackgroundResponses = true };
|
||||
|
||||
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(inputMessages, options: runOptions));
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
AgentRunOptions runOptions = new() { AllowBackgroundResponses = true };
|
||||
|
||||
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions))
|
||||
{
|
||||
// Should not reach here
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
Reference in New Issue
Block a user