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); + } }