From 82aafd36364c8cf8b2798201ac7b33c3ff064bf4 Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Thu, 20 Feb 2025 08:14:57 -0800
Subject: [PATCH 1/5] .Net Agents - Refine client provider/factory (#10616)
### Motivation and Context
Move away from the "client-provider" pattern and make client factory
more discoverable.
Fixes: https://github.com/microsoft/semantic-kernel/issues/10582
### Description
Expose ability to create an SDK client as a static factory methods on
the agent. This is more discoverable than poking around for the
client-provider and aligns with the Python approach.
- Organized factory code in a separate file from the core agent
abstractions.
- Updated samples
### Contribution Checklist
- [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 :smile:
---
.../AzureAI/AzureAIAgent.ClientFactory.cs | 65 ++++++++++
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 2 +-
.../OpenAIAssistantAgent.ClientFactory.cs | 122 ++++++++++++++++++
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +-
.../AgentUtilities/BaseAssistantTest.cs | 11 +-
.../samples/AgentUtilities/BaseAzureTest.cs | 6 +-
6 files changed, 196 insertions(+), 12 deletions(-)
create mode 100644 dotnet/src/Agents/AzureAI/AzureAIAgent.ClientFactory.cs
create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.ClientFactory.cs
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.ClientFactory.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.ClientFactory.cs
new file mode 100644
index 000000000000..f17a977ccd24
--- /dev/null
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.ClientFactory.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Net.Http;
+using Azure.AI.Projects;
+using Azure.Core;
+using Azure.Core.Pipeline;
+using Microsoft.SemanticKernel.Http;
+
+namespace Microsoft.SemanticKernel.Agents.AzureAI;
+
+///
+/// Provides an for use by .
+///
+public sealed partial class AzureAIAgent : KernelAgent
+{
+ ///
+ /// Produces a .
+ ///
+ /// The Azure AI Foundry project connection string, in the form `endpoint;subscription_id;resource_group_name;project_name`.
+ /// A credential used to authenticate to an Azure Service.
+ /// A custom for HTTP requests.
+ public static AIProjectClient CreateAzureAIClient(
+ string connectionString,
+ TokenCredential credential,
+ HttpClient? httpClient = null)
+ {
+ Verify.NotNullOrWhiteSpace(connectionString, nameof(connectionString));
+ Verify.NotNull(credential, nameof(credential));
+
+ AIProjectClientOptions clientOptions = CreateAzureClientOptions(httpClient);
+
+ return new AIProjectClient(connectionString, credential, clientOptions);
+ }
+
+ private static AIProjectClientOptions CreateAzureClientOptions(HttpClient? httpClient)
+ {
+ AIProjectClientOptions options =
+ new()
+ {
+ Diagnostics = {
+ ApplicationId = HttpHeaderConstant.Values.UserAgent,
+ }
+ };
+
+ options.AddPolicy(new SemanticKernelHeadersPolicy(), HttpPipelinePosition.PerCall);
+
+ if (httpClient is not null)
+ {
+ options.Transport = new HttpClientTransport(httpClient);
+ // Disable retry policy if and only if a custom HttpClient is provided.
+ options.RetryPolicy = new RetryPolicy(maxRetries: 0);
+ }
+
+ return options;
+ }
+
+ private class SemanticKernelHeadersPolicy : HttpPipelineSynchronousPolicy
+ {
+ public override void OnSendingRequest(HttpMessage message)
+ {
+ message.Request.Headers.Add(
+ HttpHeaderConstant.Names.SemanticKernelVersion,
+ HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AzureAIAgent)));
+ }
+ }
+}
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 1e58be54ad9f..b860f4158533 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -15,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI;
///
/// Provides a specialized based on an Azure AI agent.
///
-public sealed class AzureAIAgent : KernelAgent
+public sealed partial class AzureAIAgent : KernelAgent
{
///
/// Provides tool definitions used when associating a file attachment to an input message:
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.ClientFactory.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.ClientFactory.cs
new file mode 100644
index 000000000000..86e90fbf4adc
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.ClientFactory.cs
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Net.Http;
+using System.Threading;
+using Azure.AI.OpenAI;
+using Azure.Core;
+using Microsoft.SemanticKernel.Http;
+using OpenAI;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+public sealed partial class OpenAIAssistantAgent : KernelAgent
+{
+ ///
+ /// Specifies a key that avoids an exception from OpenAI Client when a custom endpoint is provided without an API key.
+ ///
+ private const string SingleSpaceKey = " ";
+
+ ///
+ /// Produces an .
+ ///
+ /// The API key.
+ /// The service endpoint.
+ /// A custom for HTTP requests.
+ public static AzureOpenAIClient CreateAzureOpenAIClient(ApiKeyCredential apiKey, Uri endpoint, HttpClient? httpClient = null)
+ {
+ Verify.NotNull(apiKey, nameof(apiKey));
+ Verify.NotNull(endpoint, nameof(endpoint));
+
+ AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient);
+
+ return new AzureOpenAIClient(endpoint, apiKey!, clientOptions);
+ }
+
+ ///
+ /// Produces an .
+ ///
+ /// The credentials.
+ /// The service endpoint.
+ /// A custom for HTTP requests.
+ public static AzureOpenAIClient CreateAzureOpenAIClient(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null)
+ {
+ Verify.NotNull(credential, nameof(credential));
+ Verify.NotNull(endpoint, nameof(endpoint));
+
+ AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient);
+
+ return new AzureOpenAIClient(endpoint, credential, clientOptions);
+ }
+
+ ///
+ /// Produces an .
+ ///
+ /// An optional endpoint.
+ /// A custom for HTTP requests.
+ public static OpenAIClient CreateOpenAIClient(Uri? endpoint = null, HttpClient? httpClient = null)
+ {
+ OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
+ return new OpenAIClient(new ApiKeyCredential(SingleSpaceKey), clientOptions);
+ }
+
+ ///
+ /// Produces an .
+ ///
+ /// The API key.
+ /// An optional endpoint.
+ /// A custom for HTTP requests.
+ public static OpenAIClient CreateOpenAIClient(ApiKeyCredential apiKey, Uri? endpoint = null, HttpClient? httpClient = null)
+ {
+ OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
+ return new OpenAIClient(apiKey, clientOptions);
+ }
+
+ private static AzureOpenAIClientOptions CreateAzureClientOptions(HttpClient? httpClient)
+ {
+ AzureOpenAIClientOptions options = new()
+ {
+ UserAgentApplicationId = HttpHeaderConstant.Values.UserAgent
+ };
+
+ ConfigureClientOptions(httpClient, options);
+
+ return options;
+ }
+
+ private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient)
+ {
+ OpenAIClientOptions options = new()
+ {
+ UserAgentApplicationId = HttpHeaderConstant.Values.UserAgent,
+ Endpoint = endpoint ?? httpClient?.BaseAddress,
+ };
+
+ ConfigureClientOptions(httpClient, options);
+
+ return options;
+ }
+
+ private static void ConfigureClientOptions(HttpClient? httpClient, ClientPipelineOptions options)
+ {
+ options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall);
+
+ if (httpClient is not null)
+ {
+ options.Transport = new HttpClientPipelineTransport(httpClient);
+ options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided.
+ options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout
+ }
+ }
+
+ private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue)
+ =>
+ new((message) =>
+ {
+ if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false)
+ {
+ message.Request.Headers.Set(headerName, headerValue);
+ }
+ });
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 20ea1768a7e4..843ad863b713 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI;
///
/// Represents a specialization based on Open AI Assistant / GPT.
///
-public sealed class OpenAIAssistantAgent : KernelAgent
+public sealed partial class OpenAIAssistantAgent : KernelAgent
{
///
/// The metadata key that identifies code-interpreter content.
diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAssistantTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAssistantTest.cs
index b9dd380c5058..504194becde9 100644
--- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAssistantTest.cs
+++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAssistantTest.cs
@@ -17,15 +17,14 @@ public abstract class BaseAssistantTest : BaseAgentsTest
{
protected BaseAssistantTest(ITestOutputHelper output) : base(output)
{
- var clientProvider =
+ this.Client =
this.UseOpenAIConfig ?
- OpenAIClientProvider.ForOpenAI(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
+ OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
!string.IsNullOrWhiteSpace(this.ApiKey) ?
- OpenAIClientProvider.ForAzureOpenAI(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
- OpenAIClientProvider.ForAzureOpenAI(new AzureCliCredential(), new Uri(this.Endpoint!));
+ OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
+ OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!));
- this.Client = clientProvider.Client;
- this.AssistantClient = clientProvider.AssistantClient;
+ this.AssistantClient = this.Client.GetAssistantClient();
}
///
diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs
index 32bf490a8230..e0c937870e54 100644
--- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs
+++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs
@@ -14,10 +14,8 @@ public abstract class BaseAzureAgentTest : BaseAgentsTest
{
protected BaseAzureAgentTest(ITestOutputHelper output) : base(output)
{
- var clientProvider = AzureAIClientProvider.FromConnectionString(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential());
-
- this.Client = clientProvider.Client;
- this.AgentsClient = clientProvider.AgentsClient;
+ this.Client = AzureAIAgent.CreateAzureAIClient(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential());
+ this.AgentsClient = this.Client.GetAgentsClient();
}
///
From 2482cb9ccf386b9c17a3de2dfcc7804520deef6b Mon Sep 17 00:00:00 2001
From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Date: Thu, 20 Feb 2025 18:02:34 +0000
Subject: [PATCH 2/5] Version 1.38.0 (#10625)
### Motivation and Context
Version bump
### Description
### Contribution Checklist
- [ ] The code builds clean without any errors or warnings
- [ ] 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
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone :smile:
---
dotnet/nuget/nuget-package.props | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index 46f6b320b8ae..cae925a9c142 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -1,7 +1,7 @@
- 1.37.0
+ 1.38.0
$(VersionPrefix)-$(VersionSuffix)
$(VersionPrefix)
@@ -9,7 +9,7 @@
true
- 1.36.1
+ 1.37.0
$(NoWarn);CP0003
From 2794352b03b61ce0a9e74c11b92abd1ac1c3cc68 Mon Sep 17 00:00:00 2001
From: Tao Chen
Date: Thu, 20 Feb 2025 10:32:19 -0800
Subject: [PATCH 3/5] .Net: Add Bedrock Agent tests (#10618)
### Motivation and Context
PR for Bedrock Agent in .Net SK has been merged:
https://github.com/microsoft/semantic-kernel/pull/10443. This PR adds
tests to the integration.
### Description
Add unit tests and integration tests.
### Contribution Checklist
- [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 :smile:
---
dotnet/Directory.Packages.props | 6 +-
dotnet/src/Agents/Bedrock/BedrockAgent.cs | 8 +-
.../Agents/UnitTests/Agents.UnitTests.csproj | 1 +
.../Bedrock/BedrockAgentChannelTests.cs | 289 ++++++++++++++++
.../UnitTests/Bedrock/BedrockAgentTests.cs | 290 ++++++++++++++++
.../BedrockAgentExtensionsTests.cs | 320 ++++++++++++++++++
.../BedrockFunctionSchemaExtensionsTests.cs | 111 ++++++
.../Agents/BedrockAgentTests.cs | 238 +++++++++++++
.../IntegrationTests/IntegrationTests.csproj | 1 +
.../TestSettings/BedrockAgentConfiguration.cs | 13 +
dotnet/src/IntegrationTests/testsettings.json | 4 +
11 files changed, 1274 insertions(+), 7 deletions(-)
create mode 100644 dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentChannelTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
create mode 100644 dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockFunctionSchemaExtensionsTests.cs
create mode 100644 dotnet/src/IntegrationTests/Agents/BedrockAgentTests.cs
create mode 100644 dotnet/src/IntegrationTests/TestSettings/BedrockAgentConfiguration.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index f1d4487e92a6..fe9eb1851be3 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -5,11 +5,11 @@
true
-
-
+
+
-
+
diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
index 31f199541c6a..f01e46843ace 100644
--- a/dotnet/src/Agents/Bedrock/BedrockAgent.cs
+++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
@@ -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;
@@ -106,7 +106,7 @@ public IAsyncEnumerable 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),
@@ -202,7 +202,7 @@ public IAsyncEnumerable 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.");
}
diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
index 4d3d48c7acaa..27ef0200aa1f 100644
--- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
+++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
@@ -40,6 +40,7 @@
+
diff --git a/dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentChannelTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentChannelTests.cs
new file mode 100644
index 000000000000..03f1cfbbae1b
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentChannelTests.cs
@@ -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;
+
+///
+/// Unit testing of .
+///
+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",
+ };
+
+ ///
+ /// Verify the simple scenario of receiving messages in a .
+ ///
+ [Fact]
+ public async Task VerifyReceiveAsync()
+ {
+ // Arrange
+ BedrockAgentChannel channel = new();
+ List history = this.CreateNormalHistory();
+
+ // Act
+ await channel.ReceiveAsync(history);
+
+ // Assert
+ Assert.Equal(2, await channel.GetHistoryAsync().CountAsync());
+ }
+
+ ///
+ /// Verify the skips messages with empty content.
+ ///
+ [Fact]
+ public async Task VerifyReceiveWithEmptyContentAsync()
+ {
+ // Arrange
+ BedrockAgentChannel channel = new();
+ List history = [
+ new ChatMessageContent()
+ {
+ Role = AuthorRole.User,
+ },
+ ];
+
+ // Act
+ await channel.ReceiveAsync(history);
+
+ // Assert
+ Assert.Empty(await channel.GetHistoryAsync().ToArrayAsync());
+ }
+
+ ///
+ /// Verify the channel inserts placeholders when the message sequence is incorrect.
+ ///
+ [Fact]
+ public async Task VerifyReceiveWithIncorrectSequenceAsync()
+ {
+ // Arrange
+ BedrockAgentChannel channel = new();
+ List 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);
+ }
+
+ ///
+ /// Verify the channel empties the history when reset.
+ ///
+ [Fact]
+ public async Task VerifyResetAsync()
+ {
+ // Arrange
+ BedrockAgentChannel channel = new();
+ List 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());
+ }
+
+ ///
+ /// Verify the channel correctly prepares the history for invocation.
+ ///
+ [Fact]
+ public async Task VerifyInvokeAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ BedrockAgentChannel channel = new();
+ List history = this.CreateIncorrectSequenceHistory();
+
+ // Act
+ async Task InvokeAgent()
+ {
+ await channel.ReceiveAsync(history);
+ await foreach (var _ in channel.InvokeAsync(agent))
+ {
+ continue;
+ }
+ }
+
+ // Assert
+ await Assert.ThrowsAsync(() => InvokeAgent());
+ mockRuntimeClient.Verify(x => x.InvokeAgentAsync(
+ It.Is(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()
+ ), Times.Once);
+ }
+
+ ///
+ /// Verify the channel returns an empty stream when invoking with an empty history.
+ ///
+ [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 history = [];
+ await foreach ((bool _, ChatMessageContent Message) in channel.InvokeAsync(agent))
+ {
+ history.Add(Message);
+ }
+
+ // Assert
+ Assert.Empty(history);
+ }
+
+ ///
+ /// Verify the channel correctly prepares the history for streaming invocation.
+ ///
+ [Fact]
+ public async Task VerifyInvokeStreamAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ BedrockAgentChannel channel = new();
+ List history = this.CreateIncorrectSequenceHistory();
+
+ // Act
+ async Task InvokeAgent()
+ {
+ await channel.ReceiveAsync(history);
+ await foreach (var _ in channel.InvokeStreamingAsync(agent, []))
+ {
+ continue;
+ }
+ }
+
+ // Assert
+ await Assert.ThrowsAsync(() => InvokeAgent());
+ mockRuntimeClient.Verify(x => x.InvokeAgentAsync(
+ It.Is(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()
+ ), Times.Once);
+ }
+
+ ///
+ /// Verify the channel returns an empty stream when invoking with an empty history.
+ ///
+ [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 history = [];
+ await foreach (var message in channel.InvokeStreamingAsync(agent, []))
+ {
+ history.Add(message);
+ }
+
+ // Assert
+ Assert.Empty(history);
+ }
+
+ private List CreateNormalHistory()
+ {
+ return
+ [
+ new ChatMessageContent(AuthorRole.User, "Hi!"),
+ new ChatMessageContent(AuthorRole.Assistant, "Hi, how can I help you?"),
+ ];
+ }
+
+ private List 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, Mock) CreateMockClients()
+ {
+#pragma warning disable Moq1410 // Moq: Set MockBehavior to Strict
+ Mock mockClientConfig = new();
+ Mock mockRuntimeClientConfig = new();
+ mockClientConfig.Setup(x => x.Validate()).Verifiable();
+ mockRuntimeClientConfig.Setup(x => x.Validate()).Verifiable();
+ Mock mockClient = new(
+ "fakeAccessId",
+ "fakeSecretKey",
+ mockClientConfig.Object);
+ Mock mockRuntimeClient = new(
+ "fakeAccessId",
+ "fakeSecretKey",
+ mockRuntimeClientConfig.Object);
+#pragma warning restore Moq1410 // Moq: Set MockBehavior to Strict
+ mockRuntimeClient.Setup(x => x.InvokeAgentAsync(
+ It.IsAny(),
+ It.IsAny())
+ ).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);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentTests.cs
new file mode 100644
index 000000000000..ffc86b79662d
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Bedrock/BedrockAgentTests.cs
@@ -0,0 +1,290 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Amazon.BedrockAgent;
+using Amazon.BedrockAgent.Model;
+using Amazon.BedrockAgentRuntime;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.Bedrock;
+using Microsoft.SemanticKernel.Agents.Bedrock.Extensions;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Bedrock;
+
+///
+/// Unit testing of .
+///
+public class BedrockAgentTests
+{
+ private readonly Amazon.BedrockAgent.Model.Agent _agentModel = new()
+ {
+ AgentId = "1234567890",
+ AgentName = "testName",
+ Description = "test description",
+ Instruction = "Instruction must have at least 40 characters",
+ };
+
+ private readonly CreateAgentRequest _createAgentRequest = new()
+ {
+ AgentName = "testName",
+ Description = "test description",
+ Instruction = "Instruction must have at least 40 characters",
+ };
+
+ ///
+ /// Verify the initialization of .
+ ///
+ [Fact]
+ public void VerifyBedrockAgentDefinition()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ BedrockAgent agent = new(this._agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ // Assert
+ this.VerifyAgent(agent);
+ }
+
+ ///
+ /// Verify the creation of without specialized settings.
+ ///
+ [Fact]
+ public async Task VerifyBedrockAgentCreateAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+
+ // Act
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ // Assert
+ this.VerifyAgent(bedrockAgent);
+ }
+
+ ///
+ /// Verify the creation of with action groups.
+ ///
+ [Fact]
+ public async Task VerifyBedrockAgentCreateWithActionGroupsAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ // Mock the creation of an agent action group.
+ mockClient.Setup(x => x.CreateAgentActionGroupAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new CreateAgentActionGroupResponse());
+ // Override the sequence of calls to GetAgentAsync to return the agent status
+ // because creating an agent action group will require the agent to be prepared again.
+ mockClient.SetupSequence(x => x.GetAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.NOT_PREPARED,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARING,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARED,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARING,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARED,
+ }
+ });
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+
+ // Act
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+ await bedrockAgent.CreateCodeInterpreterActionGroupAsync();
+
+ // Assert
+ this.VerifyAgent(bedrockAgent);
+ mockClient.Verify(x => x.CreateAgentActionGroupAsync(
+ It.IsAny(),
+ default), Times.Exactly(1));
+ }
+
+ ///
+ /// Verify the creation of with a kernel.
+ ///
+ [Fact]
+ public async Task VerifyBedrockAgentCreateWithKernelAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+
+ // Act
+ Kernel kernel = new();
+ kernel.Plugins.Add(KernelPluginFactory.CreateFromType());
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object)
+ {
+ Kernel = kernel,
+ };
+
+ // Assert
+ this.VerifyAgent(bedrockAgent);
+ Assert.Single(bedrockAgent.Kernel.Plugins);
+ }
+
+ ///
+ /// Verify the creation of with kernel arguments.
+ ///
+ [Fact]
+ public async Task VerifyBedrockAgentCreateWithKernelArgumentsAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+
+ // Act
+ KernelArguments arguments = new() { { "key", "value" } };
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object)
+ {
+ Arguments = arguments,
+ };
+
+ // Assert
+ this.VerifyAgent(bedrockAgent);
+ Assert.Single(bedrockAgent.Arguments);
+ }
+
+ ///
+ /// Verify the bedrock agent returns the expected channel key.
+ ///
+ [Fact]
+ public async Task VerifyBedrockAgentChannelKeyAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+
+ // Act
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+
+ // Assert
+ Assert.Single(bedrockAgent.GetChannelKeys());
+ }
+
+ private (Mock, Mock) CreateMockClients()
+ {
+#pragma warning disable Moq1410 // Moq: Set MockBehavior to Strict
+ Mock mockClientConfig = new();
+ Mock mockRuntimeClientConfig = new();
+ mockClientConfig.Setup(x => x.Validate()).Verifiable();
+ mockRuntimeClientConfig.Setup(x => x.Validate()).Verifiable();
+ Mock mockClient = new(
+ "fakeAccessId",
+ "fakeSecretKey",
+ mockClientConfig.Object);
+ Mock mockRuntimeClient = new(
+ "fakeAccessId",
+ "fakeSecretKey",
+ mockRuntimeClientConfig.Object);
+#pragma warning restore Moq1410 // Moq: Set MockBehavior to Strict
+
+ mockClient.Setup(x => x.CreateAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new CreateAgentResponse { Agent = this._agentModel });
+
+ // After a new agent is created, its status will first be CREATING then NOT_PREPARED.
+ // Internally, we will prepare the agent for use. During preparation, the agent status
+ // will be PREPARING, then finally PREPARED.
+ mockClient.SetupSequence(x => x.GetAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.NOT_PREPARED,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARING,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARED,
+ }
+ });
+
+ return (mockClient, mockRuntimeClient);
+ }
+
+ private void VerifyAgent(BedrockAgent bedrockAgent)
+ {
+ Assert.Equal(bedrockAgent.Id, this._agentModel.AgentId);
+ Assert.Equal(bedrockAgent.Name, this._agentModel.AgentName);
+ Assert.Equal(bedrockAgent.Description, this._agentModel.Description);
+ Assert.Equal(bedrockAgent.Instructions, this._agentModel.Instruction);
+ }
+
+ private sealed class WeatherPlugin
+ {
+ [KernelFunction, Description("Provides realtime weather information.")]
+ public string Current([Description("The location to get the weather for.")] string location)
+ {
+ return $"The current weather in {location} is 72 degrees.";
+ }
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
new file mode 100644
index 000000000000..78f8c8bd67c4
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs
@@ -0,0 +1,320 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Amazon.BedrockAgent;
+using Amazon.BedrockAgent.Model;
+using Amazon.BedrockAgentRuntime;
+using Microsoft.SemanticKernel.Agents.Bedrock;
+using Microsoft.SemanticKernel.Agents.Bedrock.Extensions;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Bedrock.Extensions;
+
+///
+/// Unit testing of .
+///
+public class BedrockAgentExtensionsTests
+{
+ private readonly Amazon.BedrockAgent.Model.Agent _agentModel = new()
+ {
+ AgentId = "1234567890",
+ AgentName = "testName",
+ Description = "test description",
+ Instruction = "Instruction must have at least 40 characters",
+ };
+
+ private readonly CreateAgentRequest _createAgentRequest = new()
+ {
+ AgentName = "testName",
+ Description = "test description",
+ Instruction = "Instruction must have at least 40 characters",
+ };
+
+ ///
+ /// Verify the creation of the agent and the preparation of the agent.
+ /// The status of the agent should be checked 3 times based on the setup.
+ /// 1: Waiting for the agent to go from CREATING to NOT_PREPARED.
+ /// 2: Waiting for the agent to go from NOT_PREPARED to PREPARING.
+ /// 3: Waiting for the agent to go from PREPARING to PREPARED.
+ ///
+ [Fact]
+ public async Task VerifyCreateAndPrepareAgentAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+
+ // Act
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+
+ // Assert
+ mockClient.Verify(x => x.GetAgentAsync(
+ It.IsAny(),
+ default), Times.Exactly(3));
+ }
+
+ ///
+ /// Verify the modification and preparation of the agent is correctly performed.
+ /// The status of the agent should be go through the following states:
+ /// PREPARED -> PREPARING -> PREPARED.
+ ///
+ [Fact]
+ public async Task VerifyAssociateAgentKnowledgeBaseAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ this.ModifyMockClientGetAgentResponseSequence(mockClient);
+
+ mockClient.Setup(x => x.AssociateAgentKnowledgeBaseAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new AssociateAgentKnowledgeBaseResponse());
+
+ // Act
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+ await bedrockAgent.AssociateAgentKnowledgeBaseAsync("testKnowledgeBaseId", "testKnowledgeBaseDescription");
+
+ // Assert
+ mockClient.Verify(x => x.GetAgentAsync(
+ It.IsAny(),
+ default), Times.Exactly(5));
+ }
+
+ ///
+ /// Verify the modification and preparation of the agent is correctly performed.
+ /// The status of the agent should be go through the following states:
+ /// PREPARED -> PREPARING -> PREPARED.
+ ///
+ [Fact]
+ public async Task VerifyDisassociateAgentKnowledgeBaseAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ this.ModifyMockClientGetAgentResponseSequence(mockClient);
+
+ mockClient.Setup(x => x.DisassociateAgentKnowledgeBaseAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new DisassociateAgentKnowledgeBaseResponse());
+
+ // Act
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+ await bedrockAgent.DisassociateAgentKnowledgeBaseAsync("testKnowledgeBaseId");
+
+ // Assert
+ mockClient.Verify(x => x.GetAgentAsync(
+ It.IsAny(),
+ default), Times.Exactly(5));
+ }
+
+ ///
+ /// Verify the modification and preparation of the agent is correctly performed.
+ /// The status of the agent should be go through the following states:
+ /// PREPARED -> PREPARING -> PREPARED.
+ ///
+ [Fact]
+ public async Task VerifyCreateCodeInterpreterActionGroupAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ this.ModifyMockClientGetAgentResponseSequence(mockClient);
+
+ mockClient.Setup(x => x.CreateAgentActionGroupAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new CreateAgentActionGroupResponse());
+
+ // Act
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+ await bedrockAgent.CreateCodeInterpreterActionGroupAsync();
+
+ // Assert
+ mockClient.Verify(x => x.GetAgentAsync(
+ It.IsAny(),
+ default), Times.Exactly(5));
+ }
+
+ ///
+ /// Verify the modification and preparation of the agent is correctly performed.
+ /// The status of the agent should be go through the following states:
+ /// PREPARED -> PREPARING -> PREPARED.
+ ///
+ [Fact]
+ public async Task VerifyCreateKernelFunctionActionGroupAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ this.ModifyMockClientGetAgentResponseSequence(mockClient);
+
+ mockClient.Setup(x => x.CreateAgentActionGroupAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new CreateAgentActionGroupResponse());
+
+ // Act
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+ await bedrockAgent.CreateKernelFunctionActionGroupAsync();
+
+ // Assert
+ mockClient.Verify(x => x.GetAgentAsync(
+ It.IsAny(),
+ default), Times.Exactly(5));
+ }
+
+ ///
+ /// Verify the modification and preparation of the agent is correctly performed.
+ /// The status of the agent should be go through the following states:
+ /// PREPARED -> PREPARING -> PREPARED.
+ ///
+ [Fact]
+ public async Task VerifyEnableUserInputActionGroupAsync()
+ {
+ // Arrange
+ var (mockClient, mockRuntimeClient) = this.CreateMockClients();
+ this.ModifyMockClientGetAgentResponseSequence(mockClient);
+
+ mockClient.Setup(x => x.CreateAgentActionGroupAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new CreateAgentActionGroupResponse());
+
+ // Act
+ var agentModel = await mockClient.Object.CreateAndPrepareAgentAsync(this._createAgentRequest);
+ var bedrockAgent = new BedrockAgent(agentModel, mockClient.Object, mockRuntimeClient.Object);
+ await bedrockAgent.EnableUserInputActionGroupAsync();
+
+ // Assert
+ mockClient.Verify(x => x.GetAgentAsync(
+ It.IsAny(),
+ default), Times.Exactly(5));
+ }
+
+ private (Mock, Mock) CreateMockClients()
+ {
+#pragma warning disable Moq1410 // Moq: Set MockBehavior to Strict
+ Mock mockClientConfig = new();
+ Mock mockRuntimeClientConfig = new();
+ mockClientConfig.Setup(x => x.Validate()).Verifiable();
+ mockRuntimeClientConfig.Setup(x => x.Validate()).Verifiable();
+ Mock mockClient = new(
+ "fakeAccessId",
+ "fakeSecretKey",
+ mockClientConfig.Object);
+ Mock mockRuntimeClient = new(
+ "fakeAccessId",
+ "fakeSecretKey",
+ mockRuntimeClientConfig.Object);
+#pragma warning restore Moq1410 // Moq: Set MockBehavior to Strict
+
+ mockClient.Setup(x => x.CreateAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new CreateAgentResponse { Agent = this._agentModel });
+
+ // After a new agent is created, its status will first be CREATING then NOT_PREPARED.
+ // Internally, we will prepare the agent for use. During preparation, the agent status
+ // will be PREPARING, then finally PREPARED.
+ mockClient.SetupSequence(x => x.GetAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.NOT_PREPARED,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARING,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARED,
+ }
+ });
+
+ return (mockClient, mockRuntimeClient);
+ }
+
+ ///
+ /// Modify the mock client to return a new sequence of responses for the GetAgentAsync method
+ /// that reflect the correct sequence of status change when modifying the agent.
+ ///
+ private void ModifyMockClientGetAgentResponseSequence(Mock mockClient)
+ {
+ mockClient.SetupSequence(x => x.GetAgentAsync(
+ It.IsAny(),
+ default)
+ ).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.NOT_PREPARED,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARING,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARED,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARING,
+ }
+ }).ReturnsAsync(new GetAgentResponse
+ {
+ Agent = new Amazon.BedrockAgent.Model.Agent()
+ {
+ AgentId = this._agentModel.AgentId,
+ AgentName = this._agentModel.AgentName,
+ Description = this._agentModel.Description,
+ Instruction = this._agentModel.Instruction,
+ AgentStatus = AgentStatus.PREPARED,
+ }
+ });
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockFunctionSchemaExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockFunctionSchemaExtensionsTests.cs
new file mode 100644
index 000000000000..a679fe30f83f
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockFunctionSchemaExtensionsTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.ComponentModel;
+using Amazon.BedrockAgentRuntime.Model;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.Bedrock.Extensions;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Bedrock.Extensions;
+
+///
+/// Unit testing of .
+///
+public class BedrockFunctionSchemaExtensionsTests
+{
+ ///
+ /// Verify the conversion of a to a .
+ ///
+ [Fact]
+ public void VerifyFromFunctionParameters()
+ {
+ // Arrange
+ List parameters =
+ [
+ new FunctionParameter()
+ {
+ Name = "TestParameter",
+ Type = Amazon.BedrockAgent.Type.String,
+ },
+ ];
+
+ // Act
+ KernelArguments arguments = parameters.FromFunctionParameters(null);
+
+ // Assert
+ Assert.Single(arguments);
+ Assert.True(arguments.ContainsName("TestParameter"));
+ }
+
+ ///
+ /// Verify the conversion of a to a with existing arguments.
+ ///
+ [Fact]
+ public void VerifyFromFunctionParametersWithArguments()
+ {
+ // Arrange
+ List parameters =
+ [
+ new FunctionParameter()
+ {
+ Name = "TestParameter",
+ Type = Amazon.BedrockAgent.Type.String,
+ },
+ ];
+
+ KernelArguments arguments = new()
+ {
+ { "ExistingParameter", "ExistingValue" }
+ };
+
+ // Act
+ KernelArguments updatedArguments = parameters.FromFunctionParameters(arguments);
+
+ // Assert
+ Assert.Equal(2, updatedArguments.Count);
+ Assert.True(updatedArguments.ContainsName("TestParameter"));
+ Assert.True(updatedArguments.ContainsName("ExistingParameter"));
+ }
+
+ ///
+ /// Verify the conversion of a plugin to a .
+ ///
+ [Fact]
+ public void VerifyToFunctionSchema()
+ {
+ // Arrange
+ (Kernel kernel, KernelFunction function, KernelParameterMetadata parameter) = this.CreateKernelPlugin();
+
+ // Act
+ Amazon.BedrockAgent.Model.FunctionSchema schema = kernel.ToFunctionSchema();
+
+ // Assert
+ Assert.Single(schema.Functions);
+ Assert.Equal(function.Name, schema.Functions[0].Name);
+ Assert.Equal(function.Description, schema.Functions[0].Description);
+ Assert.True(schema.Functions[0].Parameters.ContainsKey(parameter.Name));
+ Assert.Equal(parameter.Description, schema.Functions[0].Parameters[parameter.Name].Description);
+ Assert.True(schema.Functions[0].Parameters[parameter.Name].Required);
+ Assert.Equal(Amazon.BedrockAgent.Type.String, schema.Functions[0].Parameters[parameter.Name].Type);
+ Assert.Equal(Amazon.BedrockAgent.RequireConfirmation.DISABLED, schema.Functions[0].RequireConfirmation);
+ }
+
+ private (Kernel, KernelFunction, KernelParameterMetadata) CreateKernelPlugin()
+ {
+ Kernel kernel = new();
+ kernel.Plugins.Add(KernelPluginFactory.CreateFromType());
+ var function = kernel.Plugins["WeatherPlugin"]["Current"];
+ var parameter = function.Metadata.Parameters[0];
+ return (kernel, function, parameter);
+ }
+
+ private sealed class WeatherPlugin
+ {
+ [KernelFunction, Description("Provides realtime weather information.")]
+ public string Current([Description("The location to get the weather for.")] string location)
+ {
+ return $"The current weather in {location} is 72 degrees.";
+ }
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/BedrockAgentTests.cs b/dotnet/src/IntegrationTests/Agents/BedrockAgentTests.cs
new file mode 100644
index 000000000000..417fb17c72b2
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/BedrockAgentTests.cs
@@ -0,0 +1,238 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Amazon.BedrockAgent;
+using Amazon.BedrockAgent.Model;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.Bedrock;
+using Microsoft.SemanticKernel.Agents.Bedrock.Extensions;
+using SemanticKernel.IntegrationTests.TestSettings;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Agents;
+
+#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only.
+
+public sealed class BedrockAgentTests : IDisposable
+{
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ private readonly AmazonBedrockAgentClient _client = new();
+
+ ///
+ /// Integration test for invoking a .
+ ///
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData("Why is the sky blue in one sentence?")]
+ public async Task InvokeTestAsync(string input)
+ {
+ var agentModel = await this._client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest());
+ var bedrockAgent = new BedrockAgent(agentModel, this._client);
+
+ try
+ {
+ await this.ExecuteAgentAsync(bedrockAgent, input);
+ }
+ finally
+ {
+ await this._client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id });
+ }
+ }
+
+ ///
+ /// Integration test for invoking a with streaming.
+ ///
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData("Why is the sky blue in one sentence?")]
+ public async Task InvokeStreamingTestAsync(string input)
+ {
+ var agentModel = await this._client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest());
+ var bedrockAgent = new BedrockAgent(agentModel, this._client);
+
+ try
+ {
+ await this.ExecuteAgentStreamingAsync(bedrockAgent, input);
+ }
+ finally
+ {
+ await this._client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id });
+ }
+ }
+
+ ///
+ /// Integration test for invoking a with code interpreter.
+ ///
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData(@"Create a bar chart for the following data:
+Panda 5
+Tiger 8
+Lion 3
+Monkey 6
+Dolphin 2")]
+ public async Task InvokeWithCodeInterpreterTestAsync(string input)
+ {
+ var agentModel = await this._client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest());
+ var bedrockAgent = new BedrockAgent(agentModel, this._client);
+ await bedrockAgent.CreateCodeInterpreterActionGroupAsync();
+
+ try
+ {
+ var responses = await this.ExecuteAgentAsync(bedrockAgent, input);
+ BinaryContent? binaryContent = null;
+ foreach (var response in responses)
+ {
+ if (binaryContent == null && response.Items.Count > 0)
+ {
+ binaryContent = response.Items.OfType().FirstOrDefault();
+ }
+ }
+ Assert.NotNull(binaryContent);
+ }
+ finally
+ {
+ await this._client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id });
+ }
+ }
+
+ ///
+ /// Integration test for invoking a with Kernel functions.
+ ///
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData("What is the current weather in Seattle and what is the weather forecast in Seattle?", "weather")]
+ public async Task InvokeWithKernelFunctionTestAsync(string input, string expected)
+ {
+ Kernel kernel = new();
+ kernel.Plugins.Add(KernelPluginFactory.CreateFromType());
+
+ var agentModel = await this._client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest());
+ var bedrockAgent = new BedrockAgent(agentModel, this._client)
+ {
+ Kernel = kernel,
+ };
+ await bedrockAgent.CreateKernelFunctionActionGroupAsync();
+
+ try
+ {
+ await this.ExecuteAgentAsync(bedrockAgent, input, expected);
+ }
+ finally
+ {
+ await this._client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id });
+ }
+ }
+
+ ///
+ /// Executes a with the specified input and expected output.
+ /// The output of the agent will be verified against the expected output.
+ /// If the expected output is not provided, the verification will pass as long as the output is not null or empty.
+ ///
+ /// The agent to execute.
+ /// The input to provide to the agent.
+ /// The expected output from the agent.
+ /// The chat messages returned by the agent for additional verification.
+ private async Task> ExecuteAgentAsync(BedrockAgent agent, string input, string? expected = null)
+ {
+ var responses = agent.InvokeAsync(BedrockAgent.CreateSessionId(), input, null, default);
+ string responseContent = string.Empty;
+ List chatMessages = new();
+ await foreach (var response in responses)
+ {
+ // Non-streaming invoke will only return one response.
+ responseContent = response.Content ?? string.Empty;
+ chatMessages.Add(response);
+ }
+
+ if (expected != null)
+ {
+ Assert.Contains(expected, responseContent);
+ }
+ else
+ {
+ Assert.False(string.IsNullOrEmpty(responseContent));
+ }
+
+ return chatMessages;
+ }
+
+ ///
+ /// Executes a with the specified input and expected output using streaming.
+ /// The output of the agent will be verified against the expected output.
+ /// If the expected output is not provided, the verification will pass as long as the output is not null or empty.
+ ///
+ /// The agent to execute.
+ /// The input to provide to the agent.
+ /// The expected output from the agent.
+ /// The chat messages returned by the agent for additional verification.
+ private async Task> ExecuteAgentStreamingAsync(BedrockAgent agent, string input, string? expected = null)
+ {
+ var responses = agent.InvokeStreamingAsync(BedrockAgent.CreateSessionId(), input, null, default);
+ string responseContent = string.Empty;
+ List chatMessages = new();
+ await foreach (var response in responses)
+ {
+ responseContent = response.Content ?? string.Empty;
+ chatMessages.Add(response);
+ }
+
+ if (expected != null)
+ {
+ Assert.Contains(expected, responseContent);
+ }
+ else
+ {
+ Assert.False(string.IsNullOrEmpty(responseContent));
+ }
+
+ return chatMessages;
+ }
+
+ private const string AgentName = "SKIntegrationTestAgent";
+ private const string AgentDescription = "A helpful assistant who helps users find information.";
+ private const string AgentInstruction = "You're a helpful assistant who helps users find information.";
+ private CreateAgentRequest GetCreateAgentRequest()
+ {
+ BedrockAgentConfiguration bedrockAgentSettings = this._configuration.GetSection("BedrockAgent").Get()!;
+ Assert.NotNull(bedrockAgentSettings);
+
+ return new()
+ {
+ AgentName = AgentName,
+ Description = AgentDescription,
+ Instruction = AgentInstruction,
+ AgentResourceRoleArn = bedrockAgentSettings.AgentResourceRoleArn,
+ FoundationModel = bedrockAgentSettings.FoundationModel,
+ };
+ }
+
+ public void Dispose()
+ {
+ this._client.Dispose();
+ }
+
+#pragma warning disable CA1812 // Avoid uninstantiated internal classes
+ private sealed class WeatherPlugin
+ {
+ [KernelFunction, Description("Provides realtime weather information.")]
+ public string Current([Description("The location to get the weather for.")] string location)
+ {
+ return $"The current weather in {location} is 72 degrees.";
+ }
+
+ [KernelFunction, Description("Forecast weather information.")]
+ public string Forecast([Description("The location to get the weather for.")] string location)
+ {
+ return $"The forecast for {location} is 75 degrees tomorrow.";
+ }
+ }
+#pragma warning restore CA1812 // Avoid uninstantiated internal classes
+}
\ No newline at end of file
diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj
index cd4f12741f96..06b2e839116b 100644
--- a/dotnet/src/IntegrationTests/IntegrationTests.csproj
+++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj
@@ -74,6 +74,7 @@
+
diff --git a/dotnet/src/IntegrationTests/TestSettings/BedrockAgentConfiguration.cs b/dotnet/src/IntegrationTests/TestSettings/BedrockAgentConfiguration.cs
new file mode 100644
index 000000000000..7e7f28456c2a
--- /dev/null
+++ b/dotnet/src/IntegrationTests/TestSettings/BedrockAgentConfiguration.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SemanticKernel.IntegrationTests.TestSettings;
+
+[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated",
+ Justification = "Configuration classes are instantiated through IConfiguration.")]
+internal sealed class BedrockAgentConfiguration(string agentResourceRoleArn, string foundationModel)
+{
+ public string AgentResourceRoleArn { get; set; } = agentResourceRoleArn;
+ public string FoundationModel { get; set; } = foundationModel;
+}
diff --git a/dotnet/src/IntegrationTests/testsettings.json b/dotnet/src/IntegrationTests/testsettings.json
index 22c91e9affcc..5dead0d1a7c5 100644
--- a/dotnet/src/IntegrationTests/testsettings.json
+++ b/dotnet/src/IntegrationTests/testsettings.json
@@ -116,5 +116,9 @@
"ModelId": "gpt-4",
"ApiKey": ""
}
+ },
+ "BedrockAgent": {
+ "AgentResourceRoleArn": "",
+ "FoundationModel": "anthropic.claude-3-haiku-20240307-v1:0"
}
}
\ No newline at end of file
From 99cbd4570ec637c24c48526555c38d01bdc7af66 Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Thu, 20 Feb 2025 11:31:05 -0800
Subject: [PATCH 4/5] .Net Agents - Fix typos and sample execution settings
(#10628)
### Motivation and Context
Update comments for typos and usage of `ExecutionSettings`
### Description
- Ran _xmldocs_ through AI review.
- Identified all uses of `OpenAIExecutionSettings` in agent samples
### Contribution Checklist
- [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 :smile:
---
.../ChatCompletion_FunctionTermination.cs | 9 ++++-----
.../Agents/ChatCompletion_Serialization.cs | 3 +--
.../Agents/ChatCompletion_ServiceSelection.cs | 17 ++++++++---------
.../Concepts/Agents/ChatCompletion_Streaming.cs | 3 +--
.../Agents/ComplexChat_NestedShopper.cs | 2 +-
.../GettingStartedWithAgents/Step02_Plugins.cs | 5 ++---
dotnet/src/Agents/Abstractions/Agent.cs | 4 ++--
dotnet/src/Agents/Abstractions/AgentChannel.cs | 8 ++++----
dotnet/src/Agents/Abstractions/AgentChat.cs | 6 +++---
.../Abstractions/Internal/BroadcastQueue.cs | 4 ++--
dotnet/src/Agents/Core/AgentGroupChat.cs | 2 +-
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 2 +-
.../Internal/AssistantToolResourcesFactory.cs | 2 +-
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 ++--
14 files changed, 33 insertions(+), 38 deletions(-)
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
index 48fb10ba9cdc..c72ecdb79be8 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
@@ -4,7 +4,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace Agents;
@@ -23,7 +22,7 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync()
{
Instructions = "Answer questions about the menu.",
Kernel = CreateKernelWithFilter(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
@@ -70,7 +69,7 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync()
{
Instructions = "Answer questions about the menu.",
Kernel = CreateKernelWithFilter(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
@@ -111,7 +110,7 @@ public async Task UseAutoFunctionInvocationFilterWithStreamingAgentInvocationAsy
{
Instructions = "Answer questions about the menu.",
Kernel = CreateKernelWithFilter(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
@@ -174,7 +173,7 @@ public async Task UseAutoFunctionInvocationFilterWithStreamingAgentChatAsync()
{
Instructions = "Answer questions about the menu.",
Kernel = CreateKernelWithFilter(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Serialization.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Serialization.cs
index a0494c67bd70..1bc16f452d6c 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_Serialization.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Serialization.cs
@@ -3,7 +3,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace Agents;
///
@@ -24,7 +23,7 @@ public async Task SerializeAndRestoreAgentGroupChatAsync()
Instructions = HostInstructions,
Name = HostName,
Kernel = this.CreateKernelWithChatCompletion(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
index 783524adf7f1..46ea8dea2246 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_ServiceSelection.cs
@@ -2,7 +2,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace Agents;
@@ -29,7 +28,7 @@ public async Task UseServiceSelectionWithChatCompletionAgentAsync()
new()
{
Kernel = kernel,
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyGood }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { ServiceId = ServiceKeyGood }),
};
// Define the agent targeting ServiceId = ServiceKeyBad
@@ -37,7 +36,7 @@ public async Task UseServiceSelectionWithChatCompletionAgentAsync()
new()
{
Kernel = kernel,
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyBad }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { ServiceId = ServiceKeyBad }),
};
// Define the agent with no explicit ServiceId defined
@@ -57,21 +56,21 @@ public async Task UseServiceSelectionWithChatCompletionAgentAsync()
// Invoke agent with override arguments where ServiceId = ServiceKeyGood: Expect agent response
Console.WriteLine("\n[Bad Agent: Good ServiceId Override]");
- await InvokeAgentAsync(agentBad, new(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyGood }));
+ await InvokeAgentAsync(agentBad, new(new PromptExecutionSettings() { ServiceId = ServiceKeyGood }));
// Invoke agent with override arguments where ServiceId = ServiceKeyBad: Expect failure due to invalid service key
Console.WriteLine("\n[Good Agent: Bad ServiceId Override]");
- await InvokeAgentAsync(agentGood, new(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyBad }));
+ await InvokeAgentAsync(agentGood, new(new PromptExecutionSettings() { ServiceId = ServiceKeyBad }));
Console.WriteLine("\n[Default Agent: Bad ServiceId Override]");
- await InvokeAgentAsync(agentDefault, new(new OpenAIPromptExecutionSettings() { ServiceId = ServiceKeyBad }));
+ await InvokeAgentAsync(agentDefault, new(new PromptExecutionSettings() { ServiceId = ServiceKeyBad }));
// Invoke agent with override arguments with no explicit ServiceId: Expect agent response
Console.WriteLine("\n[Good Agent: No ServiceId Override]");
- await InvokeAgentAsync(agentGood, new(new OpenAIPromptExecutionSettings()));
+ await InvokeAgentAsync(agentGood, new(new PromptExecutionSettings()));
Console.WriteLine("\n[Bad Agent: No ServiceId Override]");
- await InvokeAgentAsync(agentBad, new(new OpenAIPromptExecutionSettings()));
+ await InvokeAgentAsync(agentBad, new(new PromptExecutionSettings()));
Console.WriteLine("\n[Default Agent: No ServiceId Override]");
- await InvokeAgentAsync(agentDefault, new(new OpenAIPromptExecutionSettings()));
+ await InvokeAgentAsync(agentDefault, new(new PromptExecutionSettings()));
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(ChatCompletionAgent agent, KernelArguments? arguments = null)
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
index 6d11dd80ff91..ae9d965ff9a9 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
@@ -3,7 +3,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace Agents;
@@ -50,7 +49,7 @@ public async Task UseStreamingChatCompletionAgentWithPluginAsync()
Name = "Host",
Instructions = MenuInstructions,
Kernel = this.CreateKernelWithChatCompletion(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs
index dc9178156509..6f07fb739190 100644
--- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs
+++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs
@@ -98,7 +98,7 @@ public async Task NestedChatWithAggregatorAgentAsync()
Console.WriteLine($"! {Model}");
OpenAIPromptExecutionSettings jsonSettings = new() { ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() };
- OpenAIPromptExecutionSettings autoInvokeSettings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
+ PromptExecutionSettings autoInvokeSettings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
ChatCompletionAgent internalLeaderAgent = CreateAgent(InternalLeaderName, InternalLeaderInstructions);
ChatCompletionAgent internalGiftIdeaAgent = CreateAgent(InternalGiftIdeaAgentName, InternalGiftIdeaAgentInstructions);
diff --git a/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
index b1a2053d5395..d78c6dda0e4a 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
@@ -2,7 +2,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
using Plugins;
using Resources;
@@ -55,7 +54,7 @@ public async Task UseChatCompletionWithTemplateExecutionSettingsAsync()
PromptTemplateConfig templateConfig = KernelFunctionYaml.ToPromptTemplateConfig(autoInvokeYaml);
// Define the agent:
- // Execution-settings with auto-invocation of plubins defined via the config.
+ // Execution-settings with auto-invocation of plugins defined via the config.
ChatCompletionAgent agent =
new(templateConfig)
{
@@ -82,7 +81,7 @@ private ChatCompletionAgent CreateAgentWithPlugin(
Instructions = instructions,
Name = name,
Kernel = this.CreateKernelWithChatCompletion(),
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index eab2f6532fbf..474ff1c886f8 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -67,7 +67,7 @@ public abstract class Agent
protected internal abstract IEnumerable GetChannelKeys();
///
- /// Produce the an appropriate for the agent type.
+ /// Produce an appropriate for the agent type.
///
/// The to monitor for cancellation requests. The default is .
/// An appropriate for the agent type.
@@ -78,7 +78,7 @@ public abstract class Agent
protected internal abstract Task CreateChannelAsync(CancellationToken cancellationToken);
///
- /// Produce the an appropriate for the agent type based on the provided state.
+ /// Produce an appropriate for the agent type based on the provided state.
///
/// The channel state, as serialized
/// The to monitor for cancellation requests. The default is .
diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs
index a1c385ec6f51..e6c0f572e43e 100644
--- a/dotnet/src/Agents/Abstractions/AgentChannel.cs
+++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs
@@ -26,7 +26,7 @@ public abstract class AgentChannel
protected internal abstract string Serialize();
///
- /// Receive the conversation messages. Used when joining a conversation and also during each agent interaction..
+ /// Receive the conversation messages. Used when joining a conversation and also during each agent interaction.
///
/// The chat history at the point the channel is created.
/// The to monitor for cancellation requests. The default is .
@@ -37,7 +37,7 @@ public abstract class AgentChannel
///
/// The to monitor for cancellation requests. The default is .
///
- /// The channel wont' be reused; rather, it will be discarded and a new one created.
+ /// The channel won't be reused; rather, it will be discarded and a new one created.
///
protected internal abstract Task ResetAsync(CancellationToken cancellationToken = default);
@@ -86,7 +86,7 @@ protected internal abstract IAsyncEnumerable Invoke
public abstract class AgentChannel : AgentChannel where TAgent : Agent
{
///
- /// Process a discrete incremental interaction between a single an a .
+ /// Process a discrete incremental interaction between a single and a .
///
/// The agent actively interacting with the chat.
/// The to monitor for cancellation requests. The default is .
@@ -112,7 +112,7 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent
return this.InvokeAsync((TAgent)agent, cancellationToken);
}
///
- /// Process a discrete incremental interaction between a single an a .
+ /// Process a discrete incremental interaction between a single and a .
///
/// The agent actively interacting with the chat.
/// The receiver for the completed messages generated
diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs
index 256ad4ba8064..7549974a7a42 100644
--- a/dotnet/src/Agents/Abstractions/AgentChat.cs
+++ b/dotnet/src/Agents/Abstractions/AgentChat.cs
@@ -37,7 +37,7 @@ public abstract class AgentChat
///
/// Gets a value that indicates whether a chat operation is active. Activity is defined as
- /// any the execution of any public method.
+ /// any execution of a public method.
///
public bool IsActive => Interlocked.CompareExchange(ref this._isActive, 1, 1) > 0;
@@ -197,7 +197,7 @@ public void AddChatMessages(IReadOnlyList messages)
}
///
- /// Processes a discrete incremental interaction between a single an a .
+ /// Processes a discrete incremental interaction between a single and a .
///
/// The agent actively interacting with the chat.
/// The to monitor for cancellation requests. The default is .
@@ -256,7 +256,7 @@ protected async IAsyncEnumerable InvokeAgentAsync(
}
///
- /// Processes a discrete incremental interaction between a single an a .
+ /// Processes a discrete incremental interaction between a single and a .
///
/// The agent actively interacting with the chat.
/// The to monitor for cancellation requests. The default is .
diff --git a/dotnet/src/Agents/Abstractions/Internal/BroadcastQueue.cs b/dotnet/src/Agents/Abstractions/Internal/BroadcastQueue.cs
index b4007eec2c49..6a53ece7004d 100644
--- a/dotnet/src/Agents/Abstractions/Internal/BroadcastQueue.cs
+++ b/dotnet/src/Agents/Abstractions/Internal/BroadcastQueue.cs
@@ -26,8 +26,8 @@ internal sealed class BroadcastQueue
private readonly Dictionary _queues = [];
///
- /// Defines the yield duration when waiting on a channel-queue to synchronize.
- /// to drain.
+ /// Defines the yield duration when waiting on a channel-queue to synchronize
+ /// and drain.
///
public TimeSpan BlockDuration { get; set; } = TimeSpan.FromSeconds(0.1);
diff --git a/dotnet/src/Agents/Core/AgentGroupChat.cs b/dotnet/src/Agents/Core/AgentGroupChat.cs
index 4a352225a0f1..320410f9e11a 100644
--- a/dotnet/src/Agents/Core/AgentGroupChat.cs
+++ b/dotnet/src/Agents/Core/AgentGroupChat.cs
@@ -190,7 +190,7 @@ public async IAsyncEnumerable InvokeStreamingAsync(
///
/// The prompt template string that defines the prompt.
///
- /// On optional to use when interpreting the .
+ /// An optional to use when interpreting the .
/// The default factory is used when none is provided.
///
/// The parameter names to exclude from being HTML encoded.
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 015b0a22b0f1..3cce407bb349 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -48,7 +48,7 @@ public ChatCompletionAgent(
}
///
- /// Gets the role used for the agent instructions. Defaults to "system".
+ /// Gets the role used for agent instructions. Defaults to "system".
///
///
/// Certain versions of "O*" series (deep reasoning) models require the instructions
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs
index 7c4000dcebb0..b947ccc2a78a 100644
--- a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs
@@ -16,7 +16,7 @@ internal static class AssistantToolResourcesFactory
/// Produces a definition based on the provided parameters.
///
/// An optional vector-store-id for the 'file_search' tool
- /// An optionallist of file-identifiers for the 'code_interpreter' tool.
+ /// An optional list of file-identifiers for the 'code_interpreter' tool.
public static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds)
{
bool hasVectorStore = !string.IsNullOrWhiteSpace(vectorStoreId);
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 843ad863b713..79645d61ffe2 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -271,7 +271,7 @@ public Task CreateThreadAsync(OpenAIThreadCreationOptions? options, Canc
cancellationToken);
///
- /// Creates a new assistant thread.
+ /// Deletes an assistant thread.
///
/// The thread identifier.
/// The to monitor for cancellation requests. The default is .
@@ -320,7 +320,7 @@ public IAsyncEnumerable GetThreadMessagesAsync(string thread
/// The to monitor for cancellation requests. The default is .
/// if the assistant definition was deleted.
///
- /// An assistant-based agent is not useable after deletion.
+ /// An assistant-based agent is not usable after deletion.
///
[Obsolete("Use the OpenAI.Assistants.AssistantClient to remove or otherwise modify the Assistant definition.")]
public async Task DeleteAsync(CancellationToken cancellationToken = default)
From 5c7e7593fb7ff019ae0fdc5dafbad0c7feb60761 Mon Sep 17 00:00:00 2001
From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Date: Thu, 20 Feb 2025 22:12:30 +0000
Subject: [PATCH 5/5] .Net: Change Agents.Abstractions to depend on
SemanticKernel.Abstractions instead of SemanticKernel.Core (#10574)
### Motivation and Context
Closes #10571
### Description
### Contribution Checklist
- [ ] The code builds clean without any errors or warnings
- [ ] 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
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone :smile:
---
.../Agents/ChatCompletion_Templating.cs | 8 ++--
.../Agents/OpenAIAssistant_Templating.cs | 4 +-
.../GettingStartedWithAgents/Step01_Agent.cs | 3 +-
.../Step02_Plugins.cs | 3 +-
.../Abstractions/Agents.Abstractions.csproj | 2 +-
dotnet/src/Agents/Abstractions/KernelAgent.cs | 20 +++-----
dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 10 ++--
dotnet/src/Agents/Core/Agents.Core.csproj | 1 +
dotnet/src/Agents/Core/ChatCompletionAgent.cs | 13 ++----
.../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 12 +++--
.../Agents/UnitTests/Agents.UnitTests.csproj | 1 +
.../Core/ChatCompletionAgentTests.cs | 34 +++++++++++---
.../OpenAI/OpenAIAssistantAgentTests.cs | 7 ++-
.../Functions/KernelFunctionNoop.cs | 46 +++++++++++++++++++
.../Services/AIServiceExtensions.cs | 30 ++++++++++++
15 files changed, 145 insertions(+), 49 deletions(-)
create mode 100644 dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionNoop.cs
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Templating.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Templating.cs
index 1bcf2adbe758..7372b7df19bc 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_Templating.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Templating.cs
@@ -50,7 +50,9 @@ await InvokeChatCompletionAgentWithTemplateAsync(
"""
Write a one verse poem on the requested topic in the style of {{$style}}.
Always state the requested style of the poem.
- """);
+ """,
+ PromptTemplateConfig.SemanticKernelTemplateFormat,
+ new KernelPromptTemplateFactory());
}
[Fact]
@@ -79,8 +81,8 @@ Always state the requested style of the poem.
private async Task InvokeChatCompletionAgentWithTemplateAsync(
string instructionTemplate,
- string? templateFormat = null,
- IPromptTemplateFactory? templateFactory = null)
+ string templateFormat,
+ IPromptTemplateFactory templateFactory)
{
// Define the agent
PromptTemplateConfig templateConfig =
diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Templating.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Templating.cs
index 3fcc8f3d4dd4..4bd33e676622 100644
--- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Templating.cs
+++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Templating.cs
@@ -55,7 +55,9 @@ await InvokeAssistantAgentWithTemplateAsync(
"""
Write a one verse poem on the requested topic in the styles of {{$style}}.
Always state the requested style of the poem.
- """);
+ """,
+ PromptTemplateConfig.SemanticKernelTemplateFormat,
+ new KernelPromptTemplateFactory());
}
[Fact]
diff --git a/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
index 57756e38a34f..3807c1ebef74 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
@@ -59,10 +59,11 @@ public async Task UseTemplateForChatCompletionAgentAsync()
// Define the agent
string generateStoryYaml = EmbeddedResource.Read("GenerateStory.yaml");
PromptTemplateConfig templateConfig = KernelFunctionYaml.ToPromptTemplateConfig(generateStoryYaml);
+ KernelPromptTemplateFactory templateFactory = new();
// Instructions, Name and Description properties defined via the config.
ChatCompletionAgent agent =
- new(templateConfig)
+ new(templateConfig, templateFactory)
{
Kernel = this.CreateKernelWithChatCompletion(),
Arguments =
diff --git a/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
index d78c6dda0e4a..ced4148a7287 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
@@ -52,11 +52,12 @@ public async Task UseChatCompletionWithTemplateExecutionSettingsAsync()
// Read the template resource
string autoInvokeYaml = EmbeddedResource.Read("AutoInvokeTools.yaml");
PromptTemplateConfig templateConfig = KernelFunctionYaml.ToPromptTemplateConfig(autoInvokeYaml);
+ KernelPromptTemplateFactory templateFactory = new();
// Define the agent:
// Execution-settings with auto-invocation of plugins defined via the config.
ChatCompletionAgent agent =
- new(templateConfig)
+ new(templateConfig, templateFactory)
{
Kernel = this.CreateKernelWithChatCompletion()
};
diff --git a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
index 364e8419f0d1..fa5b1daa4910 100644
--- a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
+++ b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
@@ -30,7 +30,7 @@
-
+
diff --git a/dotnet/src/Agents/Abstractions/KernelAgent.cs b/dotnet/src/Agents/Abstractions/KernelAgent.cs
index 6a46599a1788..4ee3dc332c10 100644
--- a/dotnet/src/Agents/Abstractions/KernelAgent.cs
+++ b/dotnet/src/Agents/Abstractions/KernelAgent.cs
@@ -23,9 +23,6 @@ public abstract class KernelAgent : Agent
///
/// Gets the instructions for the agent (optional).
///
- ///
- /// Instructions can be formatted in "semantic-kernel" template format ().
- ///
public string? Instructions { get; init; }
///
@@ -39,7 +36,7 @@ public abstract class KernelAgent : Agent
///
/// Gets or sets a prompt template based on the agent instructions.
///
- public IPromptTemplate? Template { get; protected set; }
+ protected IPromptTemplate? Template { get; set; }
///
protected override ILoggerFactory ActiveLoggerFactory => this.LoggerFactory ?? this.Kernel.LoggerFactory;
@@ -53,19 +50,14 @@ public abstract class KernelAgent : Agent
/// The formatted system instructions for the agent.
protected async Task FormatInstructionsAsync(Kernel kernel, KernelArguments? arguments, CancellationToken cancellationToken)
{
- // If is not set, default instructions may be treated as "semantic-kernel" template.
- if (this.Template == null)
+ // Use the provided template as the instructions
+ if (this.Template is not null)
{
- if (string.IsNullOrWhiteSpace(this.Instructions))
- {
- return null;
- }
-
- KernelPromptTemplateFactory templateFactory = new(this.LoggerFactory);
- this.Template = templateFactory.Create(new PromptTemplateConfig(this.Instructions!));
+ return await this.Template.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
}
- return await this.Template.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
+ // Use the instructions as-is
+ return this.Instructions;
}
///
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index b860f4158533..dfee5cccfb77 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -54,10 +54,12 @@ public static class Tools
///
/// The agent model definition.
/// An instance.
+ /// The prompt template configuration.
/// An optional template factory.
public AzureAIAgent(
Azure.AI.Projects.Agent model,
AgentsClient client,
+ PromptTemplateConfig? templateConfig = null,
IPromptTemplateFactory? templateFactory = null)
{
this.Client = client;
@@ -65,12 +67,12 @@ public AzureAIAgent(
this.Description = this.Definition.Description;
this.Id = this.Definition.Id;
this.Name = this.Definition.Name;
- this.Instructions = this.Definition.Instructions;
+ this.Instructions = templateConfig?.Template ?? this.Definition.Instructions;
- if (templateFactory != null)
+ if (templateConfig is not null)
{
- PromptTemplateConfig templateConfig = new(this.Instructions);
- this.Template = templateFactory.Create(templateConfig);
+ this.Template = templateFactory?.Create(templateConfig)
+ ?? throw new KernelException($"Invalid prompt template factory {templateFactory} for format {templateConfig.TemplateFormat}");
}
}
diff --git a/dotnet/src/Agents/Core/Agents.Core.csproj b/dotnet/src/Agents/Core/Agents.Core.csproj
index 46c0aa95e196..c594e131f655 100644
--- a/dotnet/src/Agents/Core/Agents.Core.csproj
+++ b/dotnet/src/Agents/Core/Agents.Core.csproj
@@ -27,6 +27,7 @@
+
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 3cce407bb349..9aa85da55c52 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -32,19 +32,16 @@ public ChatCompletionAgent() { }
/// a .
///
/// The prompt template configuration.
- /// An optional factory to produce the for the agent.
- ///
- /// When a template factory argument isn't provided, the default is used.
- ///
+ /// The prompt template factory used to produce the for the agent.
public ChatCompletionAgent(
PromptTemplateConfig templateConfig,
- IPromptTemplateFactory? templateFactory = null)
+ IPromptTemplateFactory templateFactory)
{
this.Name = templateConfig.Name;
this.Description = templateConfig.Description;
this.Instructions = templateConfig.Template;
this.Arguments = new(templateConfig.ExecutionSettings.Values);
- this.Template = templateFactory?.Create(templateConfig);
+ this.Template = templateFactory.Create(templateConfig);
}
///
@@ -99,12 +96,10 @@ protected override Task RestoreChannelAsync(string channelState, C
internal static (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments)
{
- // Need to provide a KernelFunction to the service selector as a container for the execution-settings.
- KernelFunction nullPrompt = KernelFunctionFactory.CreateFromPrompt("placeholder", arguments?.ExecutionSettings?.Values);
(IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) =
kernel.ServiceSelector.SelectAIService(
kernel,
- nullPrompt,
+ arguments?.ExecutionSettings,
arguments ?? []);
return (chatCompletionService, executionSettings);
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 79645d61ffe2..cb0fbd0bc3eb 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -53,9 +53,10 @@ public OpenAIAssistantAgent(
this.Name = this.Definition.Name;
this.Instructions = templateConfig?.Template ?? this.Definition.Instructions;
- if (templateConfig != null)
+ if (templateConfig is not null)
{
- this.Template = templateFactory?.Create(templateConfig);
+ this.Template = templateFactory?.Create(templateConfig)
+ ?? throw new KernelException($"Invalid prompt template factory {templateFactory} for format {templateConfig.TemplateFormat}");
}
if (plugins != null)
@@ -101,7 +102,7 @@ public OpenAIAssistantAgent(
/// The containing services, plugins, and other state for use throughout the operation.
/// Required arguments that provide default template parameters, including any .
/// The prompt template configuration.
- /// An optional factory to produce the for the agent.
+ /// An prompt template factory to produce the for the agent.
/// The to monitor for cancellation requests. The default is .
/// An instance.
[Obsolete("Use the OpenAI.Assistants.AssistantClient to create an assistant (CreateAssistantFromTemplateAsync).")]
@@ -111,7 +112,7 @@ public static async Task CreateFromTemplateAsync(
Kernel kernel,
KernelArguments defaultArguments,
PromptTemplateConfig templateConfig,
- IPromptTemplateFactory? templateFactory = null,
+ IPromptTemplateFactory templateFactory,
CancellationToken cancellationToken = default)
{
// Validate input
@@ -120,9 +121,10 @@ public static async Task CreateFromTemplateAsync(
Verify.NotNull(clientProvider, nameof(clientProvider));
Verify.NotNull(capabilities, nameof(capabilities));
Verify.NotNull(templateConfig, nameof(templateConfig));
+ Verify.NotNull(templateFactory, nameof(templateFactory));
// Ensure template is valid (avoid failure after posting assistant creation)
- IPromptTemplate? template = templateFactory?.Create(templateConfig);
+ IPromptTemplate template = templateFactory.Create(templateConfig);
// Create the client
AssistantClient client = clientProvider.Client.GetAssistantClient();
diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
index 27ef0200aa1f..752bd3c1ebcb 100644
--- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
+++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
@@ -35,6 +35,7 @@
+
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
index 7dd55ff290cd..1ce8039b250d 100644
--- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
@@ -46,7 +46,7 @@ public void VerifyChatCompletionAgentDefinition()
[Fact]
public void VerifyChatCompletionAgentTemplate()
{
- PromptTemplateConfig config =
+ PromptTemplateConfig promptConfig =
new()
{
Name = "TestName",
@@ -73,16 +73,38 @@ public void VerifyChatCompletionAgentTemplate()
},
}
};
+ KernelPromptTemplateFactory templateFactory = new();
// Arrange
- ChatCompletionAgent agent = new(config);
+ ChatCompletionAgent agent = new(promptConfig, templateFactory);
// Assert
Assert.NotNull(agent.Id);
- Assert.Equal(config.Template, agent.Instructions);
- Assert.Equal(config.Description, agent.Description);
- Assert.Equal(config.Name, agent.Name);
- Assert.Equal(config.ExecutionSettings, agent.Arguments.ExecutionSettings);
+ Assert.Equal(promptConfig.Template, agent.Instructions);
+ Assert.Equal(promptConfig.Description, agent.Description);
+ Assert.Equal(promptConfig.Name, agent.Name);
+ Assert.Equal(promptConfig.ExecutionSettings, agent.Arguments.ExecutionSettings);
+ }
+
+ ///
+ /// Verify throws when invalid is provided.
+ ///
+ [Fact]
+ public void VerifyThrowsForInvalidTemplateFactory()
+ {
+ // Arrange
+ PromptTemplateConfig promptConfig =
+ new()
+ {
+ Name = "TestName",
+ Description = "TestDescription",
+ Template = "TestInstructions",
+ TemplateFormat = "handlebars",
+ };
+ KernelPromptTemplateFactory templateFactory = new();
+
+ // Act and Assert
+ Assert.Throws(() => new ChatCompletionAgent(promptConfig, templateFactory));
}
///
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs
index 692938564f9c..3860855b986d 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs
@@ -10,6 +10,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
using OpenAI.Assistants;
using Xunit;
@@ -76,11 +77,9 @@ public async Task VerifyOpenAIAssistantAgentCreationDefaultTemplateAsync()
OpenAIAssistantCapabilities capabilities = new("testmodel");
- // Act and Assert
- await this.VerifyAgentTemplateAsync(capabilities, templateConfig);
-
// Act and Assert
await this.VerifyAgentTemplateAsync(capabilities, templateConfig, new KernelPromptTemplateFactory());
+ await Assert.ThrowsAsync(async () => await this.VerifyAgentTemplateAsync(capabilities, templateConfig, new HandlebarsPromptTemplateFactory()));
}
///
@@ -734,7 +733,7 @@ await OpenAIAssistantAgent.CreateAsync(
private async Task VerifyAgentTemplateAsync(
OpenAIAssistantCapabilities capabilities,
PromptTemplateConfig templateConfig,
- IPromptTemplateFactory? templateFactory = null)
+ IPromptTemplateFactory templateFactory)
{
this.SetupResponse(HttpStatusCode.OK, capabilities, templateConfig);
diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionNoop.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionNoop.cs
new file mode 100644
index 000000000000..ce6ebc7eaf39
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionNoop.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Represents a kernel function that performs no operation.
+///
+[RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
+[RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
+internal sealed class KernelFunctionNoop : KernelFunction
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// Option: Prompt execution settings.
+ internal KernelFunctionNoop(IReadOnlyDictionary? executionSettings) :
+ base($"Function_{Guid.NewGuid():N}", string.Empty, [], null, executionSettings?.ToDictionary(static kv => kv.Key, static kv => kv.Value))
+ {
+ }
+
+ ///
+ public override KernelFunction Clone(string pluginName)
+ {
+ Dictionary? executionSettings = this.ExecutionSettings?.ToDictionary(kv => kv.Key, kv => kv.Value);
+ return new KernelFunctionNoop(executionSettings);
+ }
+
+ ///
+ protected override ValueTask InvokeCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken)
+ {
+ return new(new FunctionResult(this));
+ }
+
+ ///
+ protected override IAsyncEnumerable InvokeStreamingCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken)
+ {
+ return AsyncEnumerable.Empty();
+ }
+}
diff --git a/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs
index 30a3ee7794e5..24bc16a0f8e7 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Services/AIServiceExtensions.cs
@@ -2,6 +2,8 @@
#pragma warning disable CA1716 // Identifiers should not match keywords
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
@@ -109,4 +111,32 @@ public static (T, PromptExecutionSettings?) SelectAIService(
throw new KernelException(message.ToString());
}
+
+ ///
+ /// Resolves an and associated from the specified
+ /// based on a and associated .
+ ///
+ ///
+ /// Specifies the type of the required. This must be the same type
+ /// with which the service was registered in the orvia
+ /// the .
+ ///
+ /// The to use to select a service from the .
+ /// The containing services, plugins, and other state for use throughout the operation.
+ /// The dictionary of to use to select a service from the .
+ /// The function arguments.
+ /// A tuple of the selected service and the settings associated with the service (the settings may be null).
+ /// An appropriate service could not be found.
+ [RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
+ [RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
+ public static (T, PromptExecutionSettings?) SelectAIService(
+ this IAIServiceSelector selector,
+ Kernel kernel,
+ IReadOnlyDictionary? executionSettings,
+ KernelArguments arguments) where T : class, IAIService
+ {
+ // Need to provide a KernelFunction to the service selector as a container for the execution-settings.
+ KernelFunction nullPrompt = new KernelFunctionNoop(executionSettings);
+ return selector.SelectAIService(kernel, nullPrompt, arguments);
+ }
}