Skip to content

Commit

Permalink
.Net: Add Bedrock Agent tests (#10618)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
PR for Bedrock Agent in .Net SK has been merged:
#10443. This PR adds
tests to the integration.

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->
Add unit tests and integration tests.

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
  • Loading branch information
TaoChenOSU authored Feb 20, 2025
1 parent 2482cb9 commit 2794352
Show file tree
Hide file tree
Showing 11 changed files with 1,274 additions and 7 deletions.
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

0 comments on commit 2794352

Please sign in to comment.