Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Add Bedrock Agent tests #10618

Merged
merged 6 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AWSSDK.BedrockAgent" Version="3.7.416.4" />
<PackageVersion Include="AWSSDK.BedrockAgentRuntime" Version="3.7.418.3" />
<PackageVersion Include="AWSSDK.BedrockAgent" Version="4.0.0-preview.5" />
<PackageVersion Include="AWSSDK.BedrockAgentRuntime" Version="4.0.0-preview.5" />
<PackageVersion Include="AWSSDK.BedrockRuntime" Version="4.0.0-preview.5" />
<PackageVersion Include="AWSSDK.Core" Version="4.0.0-preview.5" />
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.301" />
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.0-preview.5" />
<PackageVersion Include="Azure.AI.ContentSafety" Version="1.0.0" />
<PackageVersion Include="Azure.AI.Inference" Version="1.0.0-beta.2" />
<PackageVersion Include="Azure.AI.OpenAI" Version="[2.2.0-beta.1]" />
Expand Down
8 changes: 4 additions & 4 deletions dotnet/src/Agents/Bedrock/BedrockAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public BedrockAgent(
AmazonBedrockAgentRuntimeClient? runtimeClient = null)
{
this.AgentModel = agentModel;
this.Client ??= new AmazonBedrockAgentClient();
this.RuntimeClient ??= new AmazonBedrockAgentRuntimeClient();
this.Client = client ?? new AmazonBedrockAgentClient();
this.RuntimeClient = runtimeClient ?? new AmazonBedrockAgentRuntimeClient();

this.Id = agentModel.AgentId;
this.Name = agentModel.AgentName;
Expand Down Expand Up @@ -106,7 +106,7 @@ public IAsyncEnumerable<ChatMessageContent> InvokeAsync(
KernelArguments? arguments,
CancellationToken cancellationToken = default)
{
return invokeAgentRequest.StreamingConfigurations != null && invokeAgentRequest.StreamingConfigurations.StreamFinalResponse
return invokeAgentRequest.StreamingConfigurations != null && (invokeAgentRequest.StreamingConfigurations.StreamFinalResponse ?? false)
? throw new ArgumentException("The streaming configuration must be null for non-streaming responses.")
: ActivityExtensions.RunWithActivityAsync(
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description),
Expand Down Expand Up @@ -202,7 +202,7 @@ public IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamingAsync(
StreamFinalResponse = true,
};
}
else if (!invokeAgentRequest.StreamingConfigurations.StreamFinalResponse)
else if (!(invokeAgentRequest.StreamingConfigurations.StreamFinalResponse ?? false))
{
throw new ArgumentException("The streaming configuration must have StreamFinalResponse set to true.");
}
Expand Down
1 change: 1 addition & 0 deletions dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<ProjectReference Include="..\Core\Agents.Core.csproj" />
<ProjectReference Include="..\OpenAI\Agents.OpenAI.csproj" />
<ProjectReference Include="..\AzureAI\Agents.AzureAI.csproj" />
<ProjectReference Include="..\Bedrock\Agents.Bedrock.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
289 changes: 289 additions & 0 deletions dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentChannelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Amazon.BedrockAgent;
using Amazon.BedrockAgentRuntime;
using Amazon.BedrockAgentRuntime.Model;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents.Bedrock;
using Microsoft.SemanticKernel.ChatCompletion;
using Moq;
using Xunit;

namespace SemanticKernel.Agents.UnitTests.Bedrock;

/// <summary>
/// Unit testing of <see cref="BedrockAgentChannel"/>.
/// </summary>
public class BedrockAgentChannelTests
{
private readonly Amazon.BedrockAgent.Model.Agent _agentModel = new()
{
AgentId = "1234567890",
AgentName = "testName",
Description = "test description",
Instruction = "Instruction must have at least 40 characters",
};

/// <summary>
/// Verify the simple scenario of receiving messages in a <see cref="BedrockAgentChannel"/>.
/// </summary>
[Fact]
public async Task VerifyReceiveAsync()
{
// Arrange
BedrockAgentChannel channel = new();
List<ChatMessageContent> history = this.CreateNormalHistory();

// Act
await channel.ReceiveAsync(history);

// Assert
Assert.Equal(2, await channel.GetHistoryAsync().CountAsync());
}

/// <summary>
/// Verify the <see cref="BedrockAgentChannel"/> skips messages with empty content.
/// </summary>
[Fact]
public async Task VerifyReceiveWithEmptyContentAsync()
{
// Arrange
BedrockAgentChannel channel = new();
List<ChatMessageContent> history = [
new ChatMessageContent()
{
Role = AuthorRole.User,
},
];

// Act
await channel.ReceiveAsync(history);

// Assert
Assert.Empty(await channel.GetHistoryAsync().ToArrayAsync());
}

/// <summary>
/// Verify the channel inserts placeholders when the message sequence is incorrect.
/// </summary>
[Fact]
public async Task VerifyReceiveWithIncorrectSequenceAsync()
{
// Arrange
BedrockAgentChannel channel = new();
List<ChatMessageContent> history = this.CreateIncorrectSequenceHistory();

// Act
await channel.ReceiveAsync(history);

// Assert that a user message is inserted between the two agent messages.
// Note that `GetHistoryAsync` returns the history in a reversed order.
Assert.Equal(6, await channel.GetHistoryAsync().CountAsync());
Assert.Equal(AuthorRole.User, (await channel.GetHistoryAsync().ToArrayAsync())[3].Role);
}

/// <summary>
/// Verify the channel empties the history when reset.
/// </summary>
[Fact]
public async Task VerifyResetAsync()
{
// Arrange
BedrockAgentChannel channel = new();
List<ChatMessageContent> history = this.CreateNormalHistory();

// Act
await channel.ReceiveAsync(history);

// Assert
Assert.NotEmpty(await channel.GetHistoryAsync().ToArrayAsync());

// Act
await channel.ResetAsync();

// Assert
Assert.Empty(await channel.GetHistoryAsync().ToArrayAsync());
}

/// <summary>
/// Verify the channel correctly prepares the history for invocation.
/// </summary>
[Fact]
public async Task VerifyInvokeAsync()
{
// Arrange
var (mockClient, mockRuntimeClient) = this.CreateMockClients();
BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);

BedrockAgentChannel channel = new();
List<ChatMessageContent> history = this.CreateIncorrectSequenceHistory();

// Act
async Task InvokeAgent()
{
await channel.ReceiveAsync(history);
await foreach (var _ in channel.InvokeAsync(agent))
{
continue;
}
}

// Assert
await Assert.ThrowsAsync<HttpOperationException>(() => InvokeAgent());
mockRuntimeClient.Verify(x => x.InvokeAgentAsync(
It.Is<InvokeAgentRequest>(r =>
r.AgentAliasId == BedrockAgent.WorkingDraftAgentAlias
&& r.AgentId == this._agentModel.AgentId
&& r.InputText == "[SILENCE]" // Inserted by `EnsureLastMessageIsUser`.
&& r.SessionState.ConversationHistory.Messages.Count == 6 // There is also a user message inserted between the two agent messages.
),
It.IsAny<CancellationToken>()
), Times.Once);
}

/// <summary>
/// Verify the channel returns an empty stream when invoking with an empty history.
/// </summary>
[Fact]
public async Task VerifyInvokeWithEmptyHistoryAsync()
{
// Arrange
var (mockClient, mockRuntimeClient) = this.CreateMockClients();
BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);

BedrockAgentChannel channel = new();

// Act
List<ChatMessageContent> history = [];
await foreach ((bool _, ChatMessageContent Message) in channel.InvokeAsync(agent))
{
history.Add(Message);
}

// Assert
Assert.Empty(history);
}

/// <summary>
/// Verify the channel correctly prepares the history for streaming invocation.
/// </summary>
[Fact]
public async Task VerifyInvokeStreamAsync()
{
// Arrange
var (mockClient, mockRuntimeClient) = this.CreateMockClients();
BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);

BedrockAgentChannel channel = new();
List<ChatMessageContent> history = this.CreateIncorrectSequenceHistory();

// Act
async Task InvokeAgent()
{
await channel.ReceiveAsync(history);
await foreach (var _ in channel.InvokeStreamingAsync(agent, []))
{
continue;
}
}

// Assert
await Assert.ThrowsAsync<HttpOperationException>(() => InvokeAgent());
mockRuntimeClient.Verify(x => x.InvokeAgentAsync(
It.Is<InvokeAgentRequest>(r =>
r.AgentAliasId == BedrockAgent.WorkingDraftAgentAlias
&& r.AgentId == this._agentModel.AgentId
&& r.InputText == "[SILENCE]" // Inserted by `EnsureLastMessageIsUser`.
&& r.SessionState.ConversationHistory.Messages.Count == 6 // There is also a user message inserted between the two agent messages.
),
It.IsAny<CancellationToken>()
), Times.Once);
}

/// <summary>
/// Verify the channel returns an empty stream when invoking with an empty history.
/// </summary>
[Fact]
public async Task VerifyInvokeStreamingWithEmptyHistoryAsync()
{
// Arrange
var (mockClient, mockRuntimeClient) = this.CreateMockClients();
BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);

BedrockAgentChannel channel = new();

// Act
List<StreamingChatMessageContent> history = [];
await foreach (var message in channel.InvokeStreamingAsync(agent, []))
{
history.Add(message);
}

// Assert
Assert.Empty(history);
}

private List<ChatMessageContent> CreateNormalHistory()
{
return
[
new ChatMessageContent(AuthorRole.User, "Hi!"),
new ChatMessageContent(AuthorRole.Assistant, "Hi, how can I help you?"),
];
}

private List<ChatMessageContent> CreateIncorrectSequenceHistory()
{
return
[
new ChatMessageContent(AuthorRole.User, "What is a word that starts with 'x'?"),
new ChatMessageContent(AuthorRole.Assistant, "Xylophone.")
{
AuthorName = "Agent 1"
},
new ChatMessageContent(AuthorRole.Assistant, "Xenon.")
{
AuthorName = "Agent 2"
},
new ChatMessageContent(AuthorRole.User, "Thanks!"),
new ChatMessageContent(AuthorRole.Assistant, "Is there anything else you need?")
{
AuthorName = "Agent 1"
},
];
}

private (Mock<AmazonBedrockAgentClient>, Mock<AmazonBedrockAgentRuntimeClient>) CreateMockClients()
{
#pragma warning disable Moq1410 // Moq: Set MockBehavior to Strict
Mock<AmazonBedrockAgentConfig> mockClientConfig = new();
Mock<AmazonBedrockAgentRuntimeConfig> mockRuntimeClientConfig = new();
mockClientConfig.Setup(x => x.Validate()).Verifiable();
mockRuntimeClientConfig.Setup(x => x.Validate()).Verifiable();
Mock<AmazonBedrockAgentClient> mockClient = new(
"fakeAccessId",
"fakeSecretKey",
mockClientConfig.Object);
Mock<AmazonBedrockAgentRuntimeClient> mockRuntimeClient = new(
"fakeAccessId",
"fakeSecretKey",
mockRuntimeClientConfig.Object);
#pragma warning restore Moq1410 // Moq: Set MockBehavior to Strict
mockRuntimeClient.Setup(x => x.InvokeAgentAsync(
It.IsAny<InvokeAgentRequest>(),
It.IsAny<CancellationToken>())
).ReturnsAsync(new InvokeAgentResponse()
{
// It's not important what the response is for this test.
// And it's difficult to mock the response stream.
// Tests should expect an exception to be thrown.
HttpStatusCode = System.Net.HttpStatusCode.NotFound,
});

return (mockClient, mockRuntimeClient);
}
}
Loading
Loading