diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 844739c7abaf..f1d4487e92a6 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,6 +5,8 @@ true + + @@ -60,6 +62,7 @@ + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 21f3cbc1da67..34d4ad104c10 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -445,6 +445,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugins.AI.UnitTests", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.Postgres.UnitTests", "src\Connectors\Connectors.Postgres.UnitTests\Connectors.Postgres.UnitTests.csproj", "{2A1EC0DA-AD01-4421-AADC-1DFF65C71CCC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.Bedrock", "src\Agents\Bedrock\Agents.Bedrock.csproj", "{8C658E1E-83C8-4127-B8BF-27A638A45DDD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1196,6 +1198,12 @@ Global {2A1EC0DA-AD01-4421-AADC-1DFF65C71CCC}.Publish|Any CPU.Build.0 = Debug|Any CPU {2A1EC0DA-AD01-4421-AADC-1DFF65C71CCC}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A1EC0DA-AD01-4421-AADC-1DFF65C71CCC}.Release|Any CPU.Build.0 = Release|Any CPU + {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Publish|Any CPU.Build.0 = Publish|Any CPU + {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1360,6 +1368,7 @@ Global {0C64EC81-8116-4388-87AD-BA14D4B59974} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132} {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132} {2A1EC0DA-AD01-4421-AADC-1DFF65C71CCC} = {5A7028A7-4DDF-4E4F-84A9-37CE8F8D7E89} + {8C658E1E-83C8-4127-B8BF-27A638A45DDD} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index ee798e863b7a..b5cfce829772 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -58,6 +58,7 @@ + diff --git a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step05_AzureAIAgent_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step05_AzureAIAgent_FileSearch.cs index dba8ff1264dd..361025c44832 100644 --- a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step05_AzureAIAgent_FileSearch.cs +++ b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step05_AzureAIAgent_FileSearch.cs @@ -9,7 +9,7 @@ namespace GettingStarted.AzureAgents; /// -/// Demonstrate using code-interpreter on . +/// Demonstrate using with file search. /// public class Step05_AzureAIAgent_FileSearch(ITestOutputHelper output) : BaseAzureAgentTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/README.md b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/README.md new file mode 100644 index 000000000000..083a1c71a156 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/README.md @@ -0,0 +1,38 @@ +# Concept samples on how to use AWS Bedrock agents + +## Pre-requisites + +1. You need to have an AWS account and [access to the foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-permissions.html) +2. [AWS CLI installed](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configured](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration) + +## Before running the samples + +You need to set up some user secrets to run the samples. + +### `BedrockAgent:AgentResourceRoleArn` + +On your AWS console, go to the IAM service and go to **Roles**. Find the role you want to use and click on it. You will find the ARN in the summary section. + +``` +dotnet user-secrets set "BedrockAgent:AgentResourceRoleArn" "arn:aws:iam::...:role/..." +``` + +### `BedrockAgent:FoundationModel` + +You need to make sure you have permission to access the foundation model. You can find the model ID in the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html). To see the models you have access to, find the policy attached to your role you should see a list of models you have access to under the `Resource` section. + +``` +dotnet user-secrets set "BedrockAgent:FoundationModel" "..." +``` + +### How to add the `bedrock:InvokeModelWithResponseStream` action to an IAM policy + +1. Open the [IAM console](https://console.aws.amazon.com/iam/). +2. On the left navigation pane, choose `Roles` under `Access management`. +3. Find the role you want to edit and click on it. +4. Under the `Permissions policies` tab, click on the policy you want to edit. +5. Under the `Permissions defined in this policy` section, click on the service. You should see **Bedrock** if you already have access to the Bedrock agent service. +6. Click on the service, and then click `Edit`. +7. On the right, you will be able to add an action. Find the service and search for `InvokeModelWithResponseStream`. +8. Check the box next to the action and then scroll all the way down and click `Next`. +9. Follow the prompts to save the changes. diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step01_BedrockAgent.cs b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step01_BedrockAgent.cs new file mode 100644 index 000000000000..2c4aa4355097 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step01_BedrockAgent.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Agents.Bedrock; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +namespace GettingStarted.BedrockAgents; + +/// +/// This example demonstrates how to interact with a in the most basic way. +/// +public class Step01_BedrockAgent(ITestOutputHelper output) : BaseBedrockAgentTest(output) +{ + private const string UserQuery = "Why is the sky blue in one sentence?"; + + /// + /// Demonstrates how to create a new and interact with it. + /// The agent will respond to the user query. + /// + [Fact] + public async Task UseNewAgentAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step01_BedrockAgent"); + + // Respond to user input + try + { + var responses = bedrockAgent.InvokeAsync(BedrockAgent.CreateSessionId(), UserQuery, null); + await foreach (var response in responses) + { + this.Output.WriteLine(response.Content); + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + /// + /// Demonstrates how to create a new and interact with it using streaming. + /// The agent will respond to the user query. + /// + [Fact] + public async Task UseNewAgentStreamingAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step01_BedrockAgent_Streaming"); + + // Respond to user input + try + { + var streamingResponses = bedrockAgent.InvokeStreamingAsync(BedrockAgent.CreateSessionId(), UserQuery, null); + await foreach (var response in streamingResponses) + { + this.Output.WriteLine(response.Content); + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + protected override async Task CreateAgentAsync(string agentName) + { + // Create a new agent on the Bedrock Agent service and prepare it for use + var agentModel = await this.Client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest(agentName)); + // Create a new BedrockAgent instance with the agent model and the client + // so that we can interact with the agent using Semantic Kernel contents. + return new BedrockAgent(agentModel, this.Client); + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step02_BedrockAgent_CodeInterpreter.cs b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step02_BedrockAgent_CodeInterpreter.cs new file mode 100644 index 000000000000..70bde61a9aab --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step02_BedrockAgent_CodeInterpreter.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Reflection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.Bedrock; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +namespace GettingStarted.BedrockAgents; + +/// +/// This example demonstrates how to interact with a with code interpreter enabled. +/// +public class Step02_BedrockAgent_CodeInterpreter(ITestOutputHelper output) : BaseBedrockAgentTest(output) +{ + private const string UserQuery = @"Create a bar chart for the following data: +Panda 5 +Tiger 8 +Lion 3 +Monkey 6 +Dolphin 2"; + + /// + /// Demonstrates how to create a new with code interpreter enabled and interact with it. + /// The agent will respond to the user query by creating a Python code that will be executed by the code interpreter. + /// The output of the code interpreter will be a file containing the bar chart, which will be returned to the user. + /// + [Fact] + public async Task UseAgentWithCodeInterpreterAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step02_BedrockAgent_CodeInterpreter"); + + // Respond to user input + try + { + BinaryContent? binaryContent = null; + var responses = bedrockAgent.InvokeAsync(BedrockAgent.CreateSessionId(), UserQuery, null); + await foreach (var response in responses) + { + if (response.Content != null) + { + this.Output.WriteLine(response.Content); + } + if (binaryContent == null && response.Items.Count > 0) + { + binaryContent = response.Items.OfType().FirstOrDefault(); + } + } + + if (binaryContent == null) + { + throw new InvalidOperationException("No file found in the response."); + } + + // Save the file to the same directory as the test assembly + var filePath = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + binaryContent.Metadata!["Name"]!.ToString()!); + this.Output.WriteLine($"Saving file to {filePath}"); + binaryContent.WriteToFile(filePath, overwrite: true); + + // Expected output: + // Here is the bar chart for the given data: + // [A bar chart showing the following data: + // Panda 5 + // Tiger 8 + // Lion 3 + // Monkey 6 + // Dolphin 2] + // Saving file to ... + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + protected override async Task CreateAgentAsync(string agentName) + { + // Create a new agent on the Bedrock Agent service and prepare it for use + var agentModel = await this.Client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest(agentName)); + // Create a new BedrockAgent instance with the agent model and the client + // so that we can interact with the agent using Semantic Kernel contents. + var bedrockAgent = new BedrockAgent(agentModel, this.Client); + // Create the code interpreter action group and prepare the agent for interaction + await bedrockAgent.CreateCodeInterpreterActionGroupAsync(); + + return bedrockAgent; + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step03_BedrockAgent_Functions.cs b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step03_BedrockAgent_Functions.cs new file mode 100644 index 000000000000..ab23b4be0128 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step03_BedrockAgent_Functions.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.Bedrock; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +namespace GettingStarted.BedrockAgents; + +/// +/// This example demonstrates how to interact with a with kernel functions. +/// +public class Step03_BedrockAgent_Functions(ITestOutputHelper output) : BaseBedrockAgentTest(output) +{ + /// + /// Demonstrates how to create a new with kernel functions enabled and interact with it. + /// The agent will respond to the user query by calling kernel functions to provide weather information. + /// + [Fact] + public async Task UseAgentWithFunctionsAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step03_BedrockAgent_Functions"); + + // Respond to user input + try + { + var responses = bedrockAgent.InvokeAsync( + BedrockAgent.CreateSessionId(), + "What is the weather in Seattle?", + null); + await foreach (var response in responses) + { + if (response.Content != null) + { + this.Output.WriteLine(response.Content); + } + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + /// + /// Demonstrates how to create a new with kernel functions enabled and interact with it using streaming. + /// The agent will respond to the user query by calling kernel functions to provide weather information. + /// + [Fact] + public async Task UseAgentStreamingWithFunctionsAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step03_BedrockAgent_Functions_Streaming"); + + // Respond to user input + try + { + var streamingResponses = bedrockAgent.InvokeStreamingAsync( + BedrockAgent.CreateSessionId(), + "What is the weather forecast in Seattle?", + null); + await foreach (var response in streamingResponses) + { + if (response.Content != null) + { + this.Output.WriteLine(response.Content); + } + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + /// + /// Demonstrates how to create a new with kernel functions enabled and interact with it. + /// The agent will respond to the user query by calling multiple kernel functions in parallel to provide weather information. + /// + [Fact] + public async Task UseAgentWithParallelFunctionsAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step03_BedrockAgent_Functions_Parallel"); + + // Respond to user input + try + { + var responses = bedrockAgent.InvokeAsync( + BedrockAgent.CreateSessionId(), + "What is the current weather in Seattle and what is the weather forecast in Seattle?", + null); + await foreach (var response in responses) + { + if (response.Content != null) + { + this.Output.WriteLine(response.Content); + } + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + protected override async Task CreateAgentAsync(string agentName) + { + // Create a new agent on the Bedrock Agent service and prepare it for use + var agentModel = await this.Client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest(agentName)); + // Create a new kernel with plugins + Kernel kernel = new(); + kernel.Plugins.Add(KernelPluginFactory.CreateFromType()); + // Create a new BedrockAgent instance with the agent model and the client + // so that we can interact with the agent using Semantic Kernel contents. + var bedrockAgent = new BedrockAgent(agentModel, this.Client) + { + Kernel = kernel, + }; + // Create the kernel function action group and prepare the agent for interaction + await bedrockAgent.CreateKernelFunctionActionGroupAsync(); + + return bedrockAgent; + } + + 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."; + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step04_BedrockAgent_Trace.cs b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step04_BedrockAgent_Trace.cs new file mode 100644 index 000000000000..3e1400a5115d --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step04_BedrockAgent_Trace.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Amazon.BedrockAgentRuntime.Model; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.Bedrock; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +namespace GettingStarted.BedrockAgents; + +/// +/// This example demonstrates how to interact with a and inspect the agent's thought process. +/// To learn more about different traces available, see: +/// https://docs.aws.amazon.com/bedrock/latest/userguide/trace-events.html +/// +public class Step04_BedrockAgent_Trace(ITestOutputHelper output) : BaseBedrockAgentTest(output) +{ + /// + /// Demonstrates how to inspect the thought process of a by enabling trace. + /// + [Fact] + public async Task UseAgentWithTraceAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step04_BedrockAgent_Trace"); + + // Respond to user input + var userQuery = "What is the current weather in Seattle and what is the weather forecast in Seattle?"; + try + { + // Customize the request for advanced scenarios + InvokeAgentRequest invokeAgentRequest = new() + { + AgentAliasId = BedrockAgent.WorkingDraftAgentAlias, + AgentId = bedrockAgent.Id, + SessionId = BedrockAgent.CreateSessionId(), + InputText = userQuery, + // Enable trace to inspect the agent's thought process + EnableTrace = true, + }; + + var responses = bedrockAgent.InvokeAsync(invokeAgentRequest, null); + await foreach (var response in responses) + { + if (response.Content != null) + { + this.Output.WriteLine(response.Content); + } + if (response.InnerContent is List innerContents) + { + // There could be multiple traces and they are stored in the InnerContent property + var traceParts = innerContents.OfType().ToList(); + if (traceParts is not null) + { + foreach (var tracePart in traceParts) + { + this.OutputTrace(tracePart.Trace); + } + } + } + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + /// + /// Outputs the trace information to the console. + /// This only outputs the orchestration trace for demonstration purposes. + /// To learn more about different traces available, see: + /// https://docs.aws.amazon.com/bedrock/latest/userguide/trace-events.html + /// + private void OutputTrace(Trace trace) + { + if (trace.OrchestrationTrace is not null) + { + if (trace.OrchestrationTrace.ModelInvocationInput is not null) + { + this.Output.WriteLine("========== Orchestration trace =========="); + this.Output.WriteLine("Orchestration input:"); + this.Output.WriteLine(trace.OrchestrationTrace.ModelInvocationInput.Text); + } + if (trace.OrchestrationTrace.ModelInvocationOutput is not null) + { + this.Output.WriteLine("========== Orchestration trace =========="); + this.Output.WriteLine("Orchestration output:"); + this.Output.WriteLine(trace.OrchestrationTrace.ModelInvocationOutput.RawResponse.Content); + this.Output.WriteLine("Usage:"); + this.Output.WriteLine($"Input token: {trace.OrchestrationTrace.ModelInvocationOutput.Metadata.Usage.InputTokens}"); + this.Output.WriteLine($"Output token: {trace.OrchestrationTrace.ModelInvocationOutput.Metadata.Usage.OutputTokens}"); + } + } + // Example output: + // ========== Orchestration trace ========== + // Orchestration input: + // {"system":"You're a helpful assistant who helps users find information.You have been provided with a set of functions to answer ... + // ========== Orchestration trace ========== + // Orchestration output: + // + // To answer this question, I will need to call the following functions: + // 1. Step04_BedrockAgent_Trace_KernelFunctions::Current to get the current weather in Seattle + // 2. Step04_BedrockAgent_Trace_KernelFunctions::Forecast to get the weather forecast in Seattle + // + // + // + // + // Step04_BedrockAgent_Trace_KernelFunctions::Current + // + // Seattle + // + // Usage: + // Input token: 617 + // Output token: 144 + // ========== Orchestration trace ========== + // Orchestration input: + // {"system":"You're a helpful assistant who helps users find information.You have been provided with a set of functions to answer ... + // ========== Orchestration trace ========== + // Orchestration output: + // Now that I have the current weather in Seattle, I will call the forecast function to get the weather forecast. + // + // + // + // Step04_BedrockAgent_Trace_KernelFunctions::Forecast + // + // Seattle + // + // Usage: + // Input token: 834 + // Output token: 87 + // ========== Orchestration trace ========== + // Orchestration input: + // {"system":"You're a helpful assistant who helps users find information.You have been provided with a set of functions to answer ... + // ========== Orchestration trace ========== + // Orchestration output: + // + // The current weather in Seattle is 72 degrees. The weather forecast for Seattle is 75 degrees tomorrow. + // Usage: + // Input token: 1003 + // Output token: 31 + } + protected override async Task CreateAgentAsync(string agentName) + { + // Create a new agent on the Bedrock Agent service and prepare it for use + var agentModel = await this.Client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest(agentName)); + // Create a new kernel with plugins + Kernel kernel = new(); + kernel.Plugins.Add(KernelPluginFactory.CreateFromType()); + // Create a new BedrockAgent instance with the agent model and the client + // so that we can interact with the agent using Semantic Kernel contents. + var bedrockAgent = new BedrockAgent(agentModel, this.Client) + { + Kernel = kernel, + }; + // Create the kernel function action group and prepare the agent for interaction + await bedrockAgent.CreateKernelFunctionActionGroupAsync(); + + return bedrockAgent; + } + + 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."; + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step05_BedrockAgent_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step05_BedrockAgent_FileSearch.cs new file mode 100644 index 000000000000..9b7b4330af33 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step05_BedrockAgent_FileSearch.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Amazon.BedrockAgentRuntime.Model; +using Microsoft.SemanticKernel.Agents.Bedrock; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +namespace GettingStarted.BedrockAgents; + +/// +/// This example demonstrates how to interact with a that is associated with a knowledge base. +/// A Bedrock Knowledge Base is a collection of documents that the agent uses to answer user queries. +/// To learn more about Bedrock Knowledge Base, see: +/// https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html +/// +public class Step05_BedrockAgent_FileSearch(ITestOutputHelper output) : BaseBedrockAgentTest(output) +{ + // Replace the KnowledgeBaseId with a valid KnowledgeBaseId + // To learn how to create a Knowledge Base, see: + // https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-create.html + private const string KnowledgeBaseId = "[KnowledgeBaseId]"; + + protected override async Task CreateAgentAsync(string agentName) + { + // Create a new agent on the Bedrock Agent service and prepare it for use + var agentModel = await this.Client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest(agentName)); + // Create a new BedrockAgent instance with the agent model and the client + // so that we can interact with the agent using Semantic Kernel contents. + var bedrockAgent = new BedrockAgent(agentModel, this.Client); + // Associate the agent with a knowledge base and prepare the agent + await bedrockAgent.AssociateAgentKnowledgeBaseAsync( + KnowledgeBaseId, + "You will find information here."); + + return bedrockAgent; + } + + /// + /// Demonstrates how to use a with file search. + /// + [Fact(Skip = "This test is skipped because it requires a valid KnowledgeBaseId.")] + public async Task UseAgentWithFileSearchAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step05_BedrockAgent_FileSearch"); + + // Respond to user input + // Assuming the knowledge base contains information about Semantic Kernel. + // Feel free to modify the user query according to the information in your knowledge base. + var userQuery = "What is Semantic Kernel?"; + try + { + // Customize the request for advanced scenarios + InvokeAgentRequest invokeAgentRequest = new() + { + AgentAliasId = BedrockAgent.WorkingDraftAgentAlias, + AgentId = bedrockAgent.Id, + SessionId = BedrockAgent.CreateSessionId(), + InputText = userQuery, + }; + + var responses = bedrockAgent.InvokeAsync(invokeAgentRequest, null, CancellationToken.None); + await foreach (var response in responses) + { + if (response.Content != null) + { + this.Output.WriteLine(response.Content); + } + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step06_BedrockAgent_AgentChat.cs b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step06_BedrockAgent_AgentChat.cs new file mode 100644 index 000000000000..b7aee9d06c7e --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/BedrockAgent/Step06_BedrockAgent_AgentChat.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Bedrock; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.BedrockAgents; + +/// +/// This example demonstrates how two agents (one of which is a Bedrock agent) can chat with each other. +/// +public class Step06_BedrockAgent_AgentChat(ITestOutputHelper output) : BaseBedrockAgentTest(output) +{ + protected override async Task CreateAgentAsync(string agentName) + { + // Create a new agent on the Bedrock Agent service and prepare it for use + var agentModel = await this.Client.CreateAndPrepareAgentAsync(this.GetCreateAgentRequest(agentName)); + // Create a new BedrockAgent instance with the agent model and the client + // so that we can interact with the agent using Semantic Kernel contents. + return new BedrockAgent(agentModel, this.Client); + } + + /// + /// Demonstrates how to put two instances in a chat. + /// + [Fact] + public async Task UseAgentWithAgentChatAsync() + { + // Create the agent + var bedrockAgent = await this.CreateAgentAsync("Step06_BedrockAgent_AgentChat"); + var chatCompletionAgent = new ChatCompletionAgent() + { + Instructions = "You're a translator who helps users understand the content in Spanish.", + Name = "Translator", + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Create a chat for agent interaction + var chat = new AgentGroupChat(bedrockAgent, chatCompletionAgent) + { + ExecutionSettings = new() + { + // Terminate after two turns: one from the bedrock agent and one from the chat completion agent. + // Note: each invoke will terminate after two turns, and we are invoking the group chat for each user query. + TerminationStrategy = new MultiTurnTerminationStrategy(2), + } + }; + + // Respond to user input + string[] userQueries = [ + "Why is the sky blue in one sentence?", + "Why do we have seasons in one sentence?" + ]; + try + { + foreach (var userQuery in userQueries) + { + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, userQuery)); + await foreach (var response in chat.InvokeAsync()) + { + if (response.Content != null) + { + this.Output.WriteLine($"[{response.AuthorName}]: {response.Content}"); + } + } + } + } + finally + { + await this.Client.DeleteAgentAsync(new() { AgentId = bedrockAgent.Id }); + } + } + + internal sealed class MultiTurnTerminationStrategy : TerminationStrategy + { + public MultiTurnTerminationStrategy(int turns) + { + this.MaximumIterations = turns; + } + + /// + protected override Task ShouldAgentTerminateAsync( + Agent agent, + IReadOnlyList history, + CancellationToken cancellationToken = default) + { + return Task.FromResult(false); + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index 8efaac336d6b..ffc4734e10d6 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -45,6 +45,7 @@ + @@ -66,4 +67,4 @@ - + \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_FileSearch.cs index 361c9c0621e9..72248118577b 100644 --- a/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_FileSearch.cs +++ b/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_FileSearch.cs @@ -9,7 +9,7 @@ namespace GettingStarted.OpenAIAssistants; /// -/// Demonstrate using code-interpreter on . +/// Demonstrate using with file search. /// public class Step05_AssistantTool_FileSearch(ITestOutputHelper output) : BaseAssistantTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/README.md b/dotnet/samples/GettingStartedWithAgents/README.md index 58569a96d90f..6c54a26c0d90 100644 --- a/dotnet/samples/GettingStartedWithAgents/README.md +++ b/dotnet/samples/GettingStartedWithAgents/README.md @@ -2,13 +2,14 @@ This project contains a step by step guide to get started with _Semantic Kernel Agents_. +## NuGet -#### NuGet: - [Microsoft.SemanticKernel.Agents.Abstractions](https://www.nuget.org/packages/Microsoft.SemanticKernel.Agents.Abstractions) - [Microsoft.SemanticKernel.Agents.Core](https://www.nuget.org/packages/Microsoft.SemanticKernel.Agents.Core) - [Microsoft.SemanticKernel.Agents.OpenAI](https://www.nuget.org/packages/Microsoft.SemanticKernel.Agents.OpenAI) -#### Source +## Source + - [Semantic Kernel Agent Framework](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Agents) The examples can be run as integration tests but their code can also be copied to stand-alone programs. @@ -50,6 +51,17 @@ Example|Description [Step05_AzureAIAgent_FileSearch](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step04_AzureAIAgent_FileSearch.cs)|How to use the file-search tool for an Azure AI agent. [Step06_AzureAIAgent_OpenAPI](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step05_AzureAIAgent_OpenAPI.cs)|How to use the Open API tool for an Azure AI agent. +### Bedrock Agent + +Example|Description +---|--- +[Step01_BedrockAgent](./BedrockAgent/Step01_BedrockAgent.cs)|How to create a Bedrock agent and interact with it in the most basic way. +[Step02_BedrockAgent_CodeInterpreter](./BedrockAgent/Step02_BedrockAgent_CodeInterpreter.cs)|How to use the code-interpreter tool with a Bedrock agent. +[Step03_BedrockAgent_Functions](./BedrockAgent/Step03_BedrockAgent_Functions.cs)|How to use kernel functions with a Bedrock agent. +[Step04_BedrockAgent_Trace](./BedrockAgent/Step04_BedrockAgent_Trace.cs)|How to enable tracing for a Bedrock agent to inspect the chain of thoughts. +[Step05_BedrockAgent_FileSearch](./BedrockAgent/Step05_BedrockAgent_FileSearch.cs)|How to use file search with a Bedrock agent (i.e. Bedrock knowledge base). +[Step06_BedrockAgent_AgentChat](./BedrockAgent/Step06_BedrockAgent_AgentChat.cs)|How to create a conversation between two agents and one of them in a Bedrock agent. + ## Legacy Agents Support for the OpenAI Assistant API was originally published in `Microsoft.SemanticKernel.Experimental.Agents` package: @@ -57,8 +69,8 @@ Support for the OpenAI Assistant API was originally published in `Microsoft.Sema This package has been superseded by _Semantic Kernel Agents_, which includes support for Open AI Assistant agents. - ## Running Examples with Filters + Examples may be explored and ran within _Visual Studio_ using _Test Explorer_. You can also run specific examples via the command-line by using test filters (`dotnet test --filter`). Type `dotnet test --help` at the command line for more details. @@ -110,13 +122,20 @@ To set your secrets with .NET Secret Manager: dotnet user-secrets set "AzureOpenAI:ApiKey" "..." ``` -5. Or Azure AI: +6. Or Azure AI: ``` dotnet user-secrets set "AzureAI:ConnectionString" "..." dotnet user-secrets set "AzureAI:ChatModelId" "gpt-4o" ``` +7. Or Bedrock: + + ``` + dotnet user-secrets set "BedrockAgent:AgentResourceRoleArn" "arn:aws:iam::...:role/..." + dotnet user-secrets set "BedrockAgent:FoundationModel" "..." + ``` + > NOTE: Azure secrets will take precedence, if both Open AI and Azure Open AI secrets are defined, unless `ForceOpenAI` is set: ``` diff --git a/dotnet/src/Agents/Bedrock/Agents.Bedrock.csproj b/dotnet/src/Agents/Bedrock/Agents.Bedrock.csproj new file mode 100644 index 000000000000..e17d43f63fcc --- /dev/null +++ b/dotnet/src/Agents/Bedrock/Agents.Bedrock.csproj @@ -0,0 +1,50 @@ + + + + + Microsoft.SemanticKernel.Agents.Bedrock + Microsoft.SemanticKernel.Agents.Bedrock + net8.0;netstandard2.0 + $(NoWarn);SKEXP0110;CA1724 + false + alpha + + + + + + + Semantic Kernel Agents - Bedrock + Defines a concrete Agent based on the Bedrock Agent Service. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs new file mode 100644 index 000000000000..31f199541c6a --- /dev/null +++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Amazon.BedrockAgent; +using Amazon.BedrockAgentRuntime; +using Amazon.BedrockAgentRuntime.Model; +using Microsoft.SemanticKernel.Agents.Bedrock.Extensions; +using Microsoft.SemanticKernel.Agents.Extensions; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Agents.Bedrock; + +/// +/// Provides a specialized for the Bedrock Agent service. +/// +public class BedrockAgent : KernelAgent +{ + internal readonly AmazonBedrockAgentClient Client; + + internal readonly AmazonBedrockAgentRuntimeClient RuntimeClient; + + internal readonly Amazon.BedrockAgent.Model.Agent AgentModel; + + /// + /// There is a default alias created by Bedrock for the working draft version of the agent. + /// https://docs.aws.amazon.com/bedrock/latest/userguide/agents-deploy.html + /// + public static readonly string WorkingDraftAgentAlias = "TSTALIASID"; + + /// + /// Initializes a new instance of the class. + /// Unlike other types of agents in Semantic Kernel, prompt templates are not supported for Bedrock agents, + /// since Bedrock agents don't support using an alternative instruction in runtime. + /// + /// The agent model of an agent that exists on the Bedrock Agent service. + /// A client used to interact with the Bedrock Agent service. + /// A client used to interact with the Bedrock Agent runtime service. + public BedrockAgent( + Amazon.BedrockAgent.Model.Agent agentModel, + AmazonBedrockAgentClient? client = null, + AmazonBedrockAgentRuntimeClient? runtimeClient = null) + { + this.AgentModel = agentModel; + this.Client ??= new AmazonBedrockAgentClient(); + this.RuntimeClient ??= new AmazonBedrockAgentRuntimeClient(); + + this.Id = agentModel.AgentId; + this.Name = agentModel.AgentName; + this.Description = agentModel.Description; + this.Instructions = agentModel.Instruction; + } + + #region static methods + + /// + /// Convenient method to create an unique session id. + /// + public static string CreateSessionId() + { + return Guid.NewGuid().ToString(); + } + + #endregion + + #region public methods + + /// + /// Invoke the Bedrock agent with the given message. + /// + /// The session id. + /// The message to send to the agent. + /// The arguments to use when invoking the agent. + /// The alias id of the agent to use. The default is the working draft alias id. + /// The to monitor for cancellation requests. The default is . + /// An of . + public IAsyncEnumerable InvokeAsync( + string sessionId, + string message, + KernelArguments? arguments, + string? agentAliasId = null, + CancellationToken cancellationToken = default) + { + var invokeAgentRequest = new InvokeAgentRequest + { + AgentAliasId = agentAliasId ?? WorkingDraftAgentAlias, + AgentId = this.Id, + SessionId = sessionId, + InputText = message, + }; + + return this.InvokeAsync(invokeAgentRequest, arguments, cancellationToken); + } + + /// + /// Invoke the Bedrock agent with the given request. Use this method when you want to customize the request. + /// + /// The request to send to the agent. + /// The arguments to use when invoking the agent. + /// The to monitor for cancellation requests. The default is . + public IAsyncEnumerable InvokeAsync( + InvokeAgentRequest invokeAgentRequest, + KernelArguments? arguments, + CancellationToken cancellationToken = default) + { + return invokeAgentRequest.StreamingConfigurations != null && invokeAgentRequest.StreamingConfigurations.StreamFinalResponse + ? throw new ArgumentException("The streaming configuration must be null for non-streaming responses.") + : ActivityExtensions.RunWithActivityAsync( + () => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description), + InvokeInternal, + cancellationToken); + + // Collect all responses from the agent and return them as a single chat message content since this + // is a non-streaming API. + // The Bedrock Agent API streams beck different types of responses, i.e. text, files, metadata, etc. + // The Bedrock Agent API also won't stream back any content when it needs to call a function. It will + // only start streaming back content after the function has been called and the response is ready. + async IAsyncEnumerable InvokeInternal() + { + ChatMessageContentItemCollection items = []; + string content = ""; + Dictionary metadata = []; + List innerContents = []; + + await foreach (var message in this.InternalInvokeAsync(invokeAgentRequest, arguments, cancellationToken).ConfigureAwait(false)) + { + items.AddRange(message.Items); + content += message.Content ?? ""; + if (message.Metadata != null) + { + foreach (var key in message.Metadata.Keys) + { + metadata[key] = message.Metadata[key]; + } + } + innerContents.Add(message.InnerContent); + } + + yield return content.Length == 0 + ? throw new KernelException("No content was returned from the agent.") + : new ChatMessageContent(AuthorRole.Assistant, content) + { + AuthorName = this.GetDisplayName(), + Items = items, + ModelId = this.AgentModel.FoundationModel, + Metadata = metadata, + InnerContent = innerContents, + }; + } + } + + /// + /// Invoke the Bedrock agent with the given request and streaming response. + /// + /// The session id. + /// The message to send to the agent. + /// The arguments to use when invoking the agent. + /// The alias id of the agent to use. The default is the working draft alias id. + /// The to monitor for cancellation requests. The default is . + /// An of . + public IAsyncEnumerable InvokeStreamingAsync( + string sessionId, + string message, + KernelArguments? arguments, + string? agentAliasId = null, + CancellationToken cancellationToken = default) + { + var invokeAgentRequest = new InvokeAgentRequest + { + AgentAliasId = agentAliasId ?? WorkingDraftAgentAlias, + AgentId = this.Id, + SessionId = sessionId, + InputText = message, + StreamingConfigurations = new() + { + StreamFinalResponse = true, + }, + }; + + return this.InvokeStreamingAsync(invokeAgentRequest, arguments, cancellationToken); + } + + /// + /// Invoke the Bedrock agent with the given request and streaming response. Use this method when you want to customize the request. + /// + /// The request to send to the agent. + /// The arguments to use when invoking the agent. + /// The to monitor for cancellation requests. The default is . + /// An of . + public IAsyncEnumerable InvokeStreamingAsync( + InvokeAgentRequest invokeAgentRequest, + KernelArguments? arguments, + CancellationToken cancellationToken = default) + { + if (invokeAgentRequest.StreamingConfigurations == null) + { + invokeAgentRequest.StreamingConfigurations = new() + { + StreamFinalResponse = true, + }; + } + else if (!invokeAgentRequest.StreamingConfigurations.StreamFinalResponse) + { + throw new ArgumentException("The streaming configuration must have StreamFinalResponse set to true."); + } + + return ActivityExtensions.RunWithActivityAsync( + () => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description), + InvokeInternal, + cancellationToken); + + async IAsyncEnumerable InvokeInternal() + { + // The Bedrock agent service has the same API for both streaming and non-streaming responses. + // We are invoking the same method as the non-streaming response with the streaming configuration set, + // and converting the chat message content to streaming chat message content. + await foreach (var chatMessageContent in this.InternalInvokeAsync(invokeAgentRequest, arguments, cancellationToken).ConfigureAwait(false)) + { + yield return new StreamingChatMessageContent(chatMessageContent.Role, chatMessageContent.Content) + { + AuthorName = chatMessageContent.AuthorName, + ModelId = chatMessageContent.ModelId, + InnerContent = chatMessageContent.InnerContent, + Metadata = chatMessageContent.Metadata, + }; + } + } + } + + #endregion + + /// + protected override IEnumerable GetChannelKeys() + { + // Return the channel keys for the BedrockAgent + yield return typeof(BedrockAgentChannel).FullName!; + } + + /// + protected override Task CreateChannelAsync(CancellationToken cancellationToken) + { + // Create and return a new BedrockAgentChannel + return Task.FromResult(new BedrockAgentChannel()); + } + + /// + protected override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken) + { + // Restore and return a BedrockAgentChannel from the given state + return Task.FromResult(new BedrockAgentChannel()); + } + + #region internal methods + + internal string CodeInterpreterActionGroupSignature { get => $"{this.GetDisplayName()}_CodeInterpreter"; } + internal string KernelFunctionActionGroupSignature { get => $"{this.GetDisplayName()}_KernelFunctions"; } + internal string UseInputActionGroupSignature { get => $"{this.GetDisplayName()}_UserInput"; } + + #endregion +} diff --git a/dotnet/src/Agents/Bedrock/BedrockAgentChannel.cs b/dotnet/src/Agents/Bedrock/BedrockAgentChannel.cs new file mode 100644 index 000000000000..1e0d40d91188 --- /dev/null +++ b/dotnet/src/Agents/Bedrock/BedrockAgentChannel.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Amazon.BedrockAgentRuntime.Model; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Extensions; +using Microsoft.SemanticKernel.Agents.Serialization; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Bedrock; + +/// +/// A specialization for use with . +/// +public class BedrockAgentChannel : AgentChannel +{ + private readonly ChatHistory _history = []; + + private const string MessagePlaceholder = "[SILENCE]"; + + /// + /// Receive messages from a group chat. + /// Bedrock requires the chat history to alternate between user and agent messages. + /// Thus, when receiving messages, the message sequence will be mutated by inserting + /// placeholder agent or user messages as needed. + /// + /// The history of messages to receive. + /// A token to monitor for cancellation requests. + protected override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken) + { + foreach (var incomingMessage in history) + { + if (string.IsNullOrEmpty(incomingMessage.Content)) + { + this.Logger.LogWarning("Received a message with no content. Skipping."); + continue; + } + + if (this._history.Count == 0 || this._history.Last().Role != incomingMessage.Role) + { + this._history.Add(incomingMessage); + } + else + { + this._history.Add + ( + new ChatMessageContent + ( + incomingMessage.Role == AuthorRole.Assistant ? AuthorRole.User : AuthorRole.Assistant, + MessagePlaceholder + ) + ); + this._history.Add(incomingMessage); + } + } + + return Task.CompletedTask; + } + + /// + protected override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( + BedrockAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (!this.PrepareAndValidateHistory()) + { + yield break; + } + + InvokeAgentRequest invokeAgentRequest = new() + { + AgentAliasId = BedrockAgent.WorkingDraftAgentAlias, + AgentId = agent.Id, + SessionId = BedrockAgent.CreateSessionId(), + InputText = this._history.Last().Content, + SessionState = this.ParseHistoryToSessionState(), + }; + await foreach (var message in agent.InvokeAsync(invokeAgentRequest, null, cancellationToken).ConfigureAwait(false)) + { + if (message.Content is not null) + { + this._history.Add(message); + // All messages from Bedrock agents are user facing, i.e., function calls are not returned as messages + yield return (true, message); + } + } + } + + /// + protected override async IAsyncEnumerable InvokeStreamingAsync( + BedrockAgent agent, + IList messages, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (!this.PrepareAndValidateHistory()) + { + yield break; + } + + InvokeAgentRequest invokeAgentRequest = new() + { + AgentAliasId = BedrockAgent.WorkingDraftAgentAlias, + AgentId = agent.Id, + SessionId = BedrockAgent.CreateSessionId(), + InputText = this._history.Last().Content, + SessionState = this.ParseHistoryToSessionState(), + }; + await foreach (var message in agent.InvokeStreamingAsync(invokeAgentRequest, null, cancellationToken).ConfigureAwait(false)) + { + if (message.Content is not null) + { + this._history.Add(new() + { + Role = AuthorRole.Assistant, + Content = message.Content, + AuthorName = message.AuthorName, + InnerContent = message.InnerContent, + ModelId = message.ModelId, + }); + // All messages from Bedrock agents are user facing, i.e., function calls are not returned as messages + yield return message; + } + } + } + + /// + protected override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) + { + return this._history.ToDescendingAsync(); + } + + /// + protected override Task ResetAsync(CancellationToken cancellationToken) + { + this._history.Clear(); + + return Task.CompletedTask; + } + + /// + protected override string Serialize() + => JsonSerializer.Serialize(ChatMessageReference.Prepare(this._history)); + + #region private methods + + private bool PrepareAndValidateHistory() + { + if (this._history.Count == 0) + { + this.Logger.LogWarning("No messages to send. Bedrock requires at least one message to start a conversation."); + return false; + } + + this.EnsureHistoryAlternates(); + this.EnsureLastMessageIsUser(); + if (string.IsNullOrEmpty(this._history.Last().Content)) + { + this.Logger.LogWarning("Last message has no content. Bedrock doesn't support empty messages."); + return false; + } + + return true; + } + + private void EnsureHistoryAlternates() + { + if (this._history.Count <= 1) + { + return; + } + + int currentIndex = 1; + while (currentIndex < this._history.Count) + { + if (this._history[currentIndex].Role == this._history[currentIndex - 1].Role) + { + this._history.Insert( + currentIndex, + new ChatMessageContent( + this._history[currentIndex].Role == AuthorRole.Assistant ? AuthorRole.User : AuthorRole.Assistant, + MessagePlaceholder + ) + ); + currentIndex += 2; + } + else + { + currentIndex++; + } + } + } + + private void EnsureLastMessageIsUser() + { + if (this._history.Count > 0 && this._history.Last().Role != AuthorRole.User) + { + this._history.Add(new ChatMessageContent(AuthorRole.User, MessagePlaceholder)); + } + } + + private SessionState ParseHistoryToSessionState() + { + SessionState sessionState = new(); + + // We don't take the last message as it needs to be sent separately in another parameter. + if (this._history.Count > 1) + { + sessionState.ConversationHistory = new() + { + Messages = [] + }; + + foreach (var message in this._history.Take(this._history.Count - 1)) + { + if (message.Content is null) + { + throw new InvalidOperationException("Message content cannot be null."); + } + if (message.Role != AuthorRole.Assistant && message.Role != AuthorRole.User) + { + throw new InvalidOperationException("Message role must be either Assistant or User."); + } + + sessionState.ConversationHistory.Messages.Add(new() + { + Role = message.Role == AuthorRole.Assistant + ? Amazon.BedrockAgentRuntime.ConversationRole.Assistant + : Amazon.BedrockAgentRuntime.ConversationRole.User, + Content = [ + new Amazon.BedrockAgentRuntime.Model.ContentBlock() + { + Text = message.Content, + }, + ], + }); + } + } + + return sessionState; + } + #endregion +} diff --git a/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs new file mode 100644 index 000000000000..c2e6bdd358bb --- /dev/null +++ b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.BedrockAgent; +using Amazon.BedrockAgent.Model; + +namespace Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +/// +/// Extensions associated with +/// +public static class BedrockAgentExtensions +{ + /// + /// Creates an agent. + /// + /// The instance. + /// The instance. + /// The instance. + public static async Task CreateAndPrepareAgentAsync( + this AmazonBedrockAgentClient client, + CreateAgentRequest request, + CancellationToken cancellationToken = default) + { + var createAgentResponse = await client.CreateAgentAsync(request, cancellationToken).ConfigureAwait(false); + // The agent will first enter the CREATING status. + // When the operation finishes, it will enter the NOT_PREPARED status. + // We need to wait for the agent to reach the NOT_PREPARED status before we can prepare it. + await client.WaitForAgentStatusAsync(createAgentResponse.Agent, AgentStatus.NOT_PREPARED, cancellationToken: cancellationToken).ConfigureAwait(false); + return await client.PrepareAgentAndWaitUntilPreparedAsync(createAgentResponse.Agent, cancellationToken).ConfigureAwait(false); + } + + /// + /// Associates an agent with a knowledge base. + /// + /// The instance. + /// The knowledge base ID. + /// The description of the association. + /// The instance. + public static async Task AssociateAgentKnowledgeBaseAsync( + this BedrockAgent agent, + string knowledgeBaseId, + string description, + CancellationToken cancellationToken = default) + { + await agent.Client.AssociateAgentKnowledgeBaseAsync(new() + { + AgentId = agent.Id, + AgentVersion = agent.AgentModel.AgentVersion ?? "DRAFT", + KnowledgeBaseId = knowledgeBaseId, + Description = description, + }, cancellationToken).ConfigureAwait(false); + + await agent.Client.PrepareAgentAndWaitUntilPreparedAsync(agent.AgentModel, cancellationToken).ConfigureAwait(false); + } + + /// + /// Disassociate the agent with a knowledge base. + /// + /// The instance. + /// The id of the knowledge base to disassociate with the agent. + /// The to monitor for cancellation requests. The default is . + public static async Task DisassociateAgentKnowledgeBaseAsync( + this BedrockAgent agent, + string knowledgeBaseId, + CancellationToken cancellationToken = default) + { + await agent.Client.DisassociateAgentKnowledgeBaseAsync(new() + { + AgentId = agent.Id, + AgentVersion = agent.AgentModel.AgentVersion ?? "DRAFT", + KnowledgeBaseId = knowledgeBaseId, + }, cancellationToken).ConfigureAwait(false); + + await agent.Client.PrepareAgentAndWaitUntilPreparedAsync(agent.AgentModel, cancellationToken).ConfigureAwait(false); + } + + /// + /// List the knowledge bases associated with the agent. + /// + /// The instance. + /// The to monitor for cancellation requests. The default is . + /// A containing the knowledge bases associated with the agent. + public static async Task ListAssociatedKnowledgeBasesAsync( + this BedrockAgent agent, + CancellationToken cancellationToken = default) + { + return await agent.Client.ListAgentKnowledgeBasesAsync(new() + { + AgentId = agent.Id, + AgentVersion = agent.AgentModel.AgentVersion ?? "DRAFT", + }, cancellationToken).ConfigureAwait(false); + } + + /// + /// Create a code interpreter action group for the agent and prepare the agent. + /// + /// The instance. + /// The to monitor for cancellation requests. The default is . + public static async Task CreateCodeInterpreterActionGroupAsync( + this BedrockAgent agent, + CancellationToken cancellationToken = default) + { + var createAgentActionGroupRequest = new CreateAgentActionGroupRequest + { + AgentId = agent.Id, + AgentVersion = agent.AgentModel.AgentVersion ?? "DRAFT", + ActionGroupName = agent.CodeInterpreterActionGroupSignature, + ActionGroupState = ActionGroupState.ENABLED, + ParentActionGroupSignature = new(Amazon.BedrockAgent.ActionGroupSignature.AMAZONCodeInterpreter), + }; + + await agent.Client.CreateAgentActionGroupAsync(createAgentActionGroupRequest, cancellationToken).ConfigureAwait(false); + await agent.Client.PrepareAgentAndWaitUntilPreparedAsync(agent.AgentModel, cancellationToken).ConfigureAwait(false); + } + + /// + /// Create a kernel function action group for the agent and prepare the agent. + /// + /// The instance. + /// The to monitor for cancellation requests. The default is . + public static async Task CreateKernelFunctionActionGroupAsync( + this BedrockAgent agent, + CancellationToken cancellationToken = default) + { + var createAgentActionGroupRequest = new CreateAgentActionGroupRequest + { + AgentId = agent.Id, + AgentVersion = agent.AgentModel.AgentVersion ?? "DRAFT", + ActionGroupName = agent.KernelFunctionActionGroupSignature, + ActionGroupState = ActionGroupState.ENABLED, + ActionGroupExecutor = new() + { + CustomControl = Amazon.BedrockAgent.CustomControlMethod.RETURN_CONTROL, + }, + FunctionSchema = agent.Kernel.ToFunctionSchema(), + }; + + await agent.Client.CreateAgentActionGroupAsync(createAgentActionGroupRequest, cancellationToken).ConfigureAwait(false); + await agent.Client.PrepareAgentAndWaitUntilPreparedAsync(agent.AgentModel, cancellationToken).ConfigureAwait(false); + } + + /// + /// Enable user input for the agent and prepare the agent. + /// + /// The instance. + /// The to monitor for cancellation requests. The default is . + public static async Task EnableUserInputActionGroupAsync( + this BedrockAgent agent, + CancellationToken cancellationToken = default) + { + var createAgentActionGroupRequest = new CreateAgentActionGroupRequest + { + AgentId = agent.Id, + AgentVersion = agent.AgentModel.AgentVersion ?? "DRAFT", + ActionGroupName = agent.UseInputActionGroupSignature, + ActionGroupState = ActionGroupState.ENABLED, + ParentActionGroupSignature = new(Amazon.BedrockAgent.ActionGroupSignature.AMAZONUserInput), + }; + + await agent.Client.CreateAgentActionGroupAsync(createAgentActionGroupRequest, cancellationToken).ConfigureAwait(false); + await agent.Client.PrepareAgentAndWaitUntilPreparedAsync(agent.AgentModel, cancellationToken).ConfigureAwait(false); + } + + private static async Task PrepareAgentAndWaitUntilPreparedAsync( + this AmazonBedrockAgentClient client, + Amazon.BedrockAgent.Model.Agent agent, + CancellationToken cancellationToken = default) + { + var prepareAgentResponse = await client.PrepareAgentAsync(new() { AgentId = agent.AgentId }, cancellationToken).ConfigureAwait(false); + + // The agent will take some time to enter the PREPARING status after the prepare operation is called. + // We need to wait for the agent to reach the PREPARING status before we can proceed, otherwise we + // will return immediately if the agent is already in PREPARED status. + await client.WaitForAgentStatusAsync(agent, AgentStatus.PREPARING, cancellationToken: cancellationToken).ConfigureAwait(false); + // When the agent is prepared, it will enter the PREPARED status. + return await client.WaitForAgentStatusAsync(agent, AgentStatus.PREPARED, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Wait for the agent to reach the specified status. + /// + /// The instance. + /// The to monitor. + /// The status to wait for. + /// The interval in seconds to wait between attempts. The default is 2 seconds. + /// The maximum number of attempts to make. The default is 5 attempts. + /// The to monitor for cancellation requests. + /// The instance. + private static async Task WaitForAgentStatusAsync( + this AmazonBedrockAgentClient client, + Amazon.BedrockAgent.Model.Agent agent, + AgentStatus status, + int interval = 2, + int maxAttempts = 5, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < maxAttempts; i++) + { + var getAgentResponse = await client.GetAgentAsync(new() { AgentId = agent.AgentId }, cancellationToken).ConfigureAwait(false); + + if (getAgentResponse.Agent.AgentStatus == status) + { + return getAgentResponse.Agent; + } + + await Task.Delay(interval * 1000, cancellationToken).ConfigureAwait(false); + } + + throw new TimeoutException($"Agent did not reach status {status} within the specified time."); + } +} diff --git a/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentInvokeExtensions.cs b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentInvokeExtensions.cs new file mode 100644 index 000000000000..5e67aacaf04a --- /dev/null +++ b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentInvokeExtensions.cs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.BedrockAgentRuntime; +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Runtime.EventStreams.Internal; +using Microsoft.SemanticKernel.Agents.Extensions; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.FunctionCalling; + +namespace Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +/// +/// Extensions associated with the status of a . +/// +internal static class BedrockAgentInvokeExtensions +{ + public static async IAsyncEnumerable InternalInvokeAsync( + this BedrockAgent agent, + InvokeAgentRequest invokeAgentRequest, + KernelArguments? arguments, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + // This session state is used to store the results of function calls to be passed back to the agent. + // https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/BedrockAgentRuntime/TSessionState.html + SessionState? sessionState = null; + for (var requestIndex = 0; ; requestIndex++) + { + if (sessionState != null) + { + invokeAgentRequest.SessionState = sessionState; + sessionState = null; + } + var invokeAgentResponse = await agent.RuntimeClient.InvokeAgentAsync(invokeAgentRequest, cancellationToken).ConfigureAwait(false); + + if (invokeAgentResponse.HttpStatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpOperationException($"Failed to invoke agent. Status code: {invokeAgentResponse.HttpStatusCode}"); + } + + List functionCallContents = []; + await foreach (var responseEvent in invokeAgentResponse.Completion.ToAsyncEnumerable().ConfigureAwait(false)) + { + if (responseEvent is BedrockAgentRuntimeEventStreamException bedrockAgentRuntimeEventStreamException) + { + throw new KernelException("Failed to handle Bedrock Agent stream event.", bedrockAgentRuntimeEventStreamException); + } + + var chatMessageContent = + HandleChunkEvent(agent, responseEvent) ?? + HandleFilesEvent(agent, responseEvent) ?? + HandleReturnControlEvent(agent, responseEvent, arguments) ?? + HandleTraceEvent(agent, responseEvent) ?? + throw new KernelException($"Failed to handle Bedrock Agent stream event: {responseEvent}"); + if (chatMessageContent.Items.Count > 0 && chatMessageContent.Items[0] is FunctionCallContent functionCallContent) + { + functionCallContents.AddRange(chatMessageContent.Items.Where(item => item is FunctionCallContent).Cast()); + } + else + { + yield return chatMessageContent; + } + } + + // This is used to cap the auto function invocation loop to prevent infinite loops. + // It doesn't use the the `FunctionCallsProcessor` to process the functions because we do not need + // many of the features it offers and we want to keep the code simple. + var functionChoiceBehaviorConfiguration = new FunctionCallsProcessor().GetConfiguration( + FunctionChoiceBehavior.Auto(), [], requestIndex, agent.Kernel); + + if (functionCallContents.Count > 0 && functionChoiceBehaviorConfiguration!.AutoInvoke) + { + var functionResults = await InvokeFunctionCallsAsync(agent, functionCallContents, cancellationToken).ConfigureAwait(false); + sessionState = CreateSessionStateWithFunctionResults(functionResults, agent); + } + else + { + break; + } + } + } + + private static ChatMessageContent? HandleChunkEvent( + BedrockAgent agent, + IEventStreamEvent responseEvent) + { + return responseEvent is not PayloadPart payload + ? null + : new ChatMessageContent() + { + Role = AuthorRole.Assistant, + AuthorName = agent.GetDisplayName(), + Content = Encoding.UTF8.GetString(payload.Bytes.ToArray()), + ModelId = agent.AgentModel.FoundationModel, + InnerContent = payload, + }; + } + + private static ChatMessageContent? HandleFilesEvent( + BedrockAgent agent, + IEventStreamEvent responseEvent) + { + if (responseEvent is not FilePart files) + { + return null; + } + + ChatMessageContentItemCollection binaryContents = []; + foreach (var file in files.Files) + { + binaryContents.Add(new BinaryContent(file.Bytes.ToArray(), file.Type) + { + Metadata = new Dictionary() + { + { "Name", file.Name }, + }, + }); + } + + return new ChatMessageContent() + { + Role = AuthorRole.Assistant, + AuthorName = agent.GetDisplayName(), + Items = binaryContents, + ModelId = agent.AgentModel.FoundationModel, + InnerContent = files, + }; + } + + private static ChatMessageContent? HandleReturnControlEvent( + BedrockAgent agent, + IEventStreamEvent responseEvent, + KernelArguments? arguments) + { + if (responseEvent is not ReturnControlPayload returnControlPayload) + { + return null; + } + + ChatMessageContentItemCollection functionCallContents = []; + foreach (var invocationInput in returnControlPayload.InvocationInputs) + { + var functionInvocationInput = invocationInput.FunctionInvocationInput; + functionCallContents.Add(new FunctionCallContent( + functionInvocationInput.Function, + id: returnControlPayload.InvocationId, + arguments: functionInvocationInput.Parameters.FromFunctionParameters(arguments)) + { + Metadata = new Dictionary() + { + { "ActionGroup", functionInvocationInput.ActionGroup }, + { "ActionInvocationType", functionInvocationInput.ActionInvocationType }, + }, + }); + } + + return new ChatMessageContent() + { + Role = AuthorRole.Assistant, + AuthorName = agent.GetDisplayName(), + Items = functionCallContents, + ModelId = agent.AgentModel.FoundationModel, + InnerContent = returnControlPayload, + }; + } + + private static ChatMessageContent? HandleTraceEvent( + BedrockAgent agent, + IEventStreamEvent responseEvent) + { + return responseEvent is not TracePart trace + ? null + : new ChatMessageContent() + { + Role = AuthorRole.Assistant, + AuthorName = agent.GetDisplayName(), + ModelId = agent.AgentModel.FoundationModel, + InnerContent = trace, + }; + } + + private static async Task> InvokeFunctionCallsAsync( + BedrockAgent agent, + List functionCallContents, + CancellationToken cancellationToken) + { + var functionResults = await Task.WhenAll(functionCallContents.Select(async functionCallContent => + { + return await functionCallContent.InvokeAsync(agent.Kernel, cancellationToken).ConfigureAwait(false); + })).ConfigureAwait(false); + + return [.. functionResults]; + } + + private static SessionState CreateSessionStateWithFunctionResults(List functionResults, BedrockAgent agent) + { + return functionResults.Count == 0 + ? throw new KernelException("No function results were returned.") + : new() + { + InvocationId = functionResults[0].CallId, + ReturnControlInvocationResults = [.. functionResults.Select(functionResult => + { + return new InvocationResultMember() + { + FunctionResult = new Amazon.BedrockAgentRuntime.Model.FunctionResult + { + ActionGroup = agent.KernelFunctionActionGroupSignature, + Function = functionResult.FunctionName, + ResponseBody = new Dictionary + { + { "TEXT", new ContentBody() { Body = functionResult.Result as string } } + } + } + }; + } + )], + }; + } +} diff --git a/dotnet/src/Agents/Bedrock/Extensions/BedrockFunctionSchemaExtensions.cs b/dotnet/src/Agents/Bedrock/Extensions/BedrockFunctionSchemaExtensions.cs new file mode 100644 index 000000000000..c890638484a2 --- /dev/null +++ b/dotnet/src/Agents/Bedrock/Extensions/BedrockFunctionSchemaExtensions.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Amazon.BedrockAgent.Model; +using Amazon.BedrockAgentRuntime.Model; + +namespace Microsoft.SemanticKernel.Agents.Bedrock.Extensions; + +/// +/// Extensions associated with the status of a . +/// +internal static class BedrockFunctionSchemaExtensions +{ + public static KernelArguments FromFunctionParameters(this List parameters, KernelArguments? arguments) + { + KernelArguments kernelArguments = arguments ?? []; + foreach (var parameter in parameters) + { + kernelArguments.Add(parameter.Name, parameter.Value); + } + + return kernelArguments; + } + + public static Amazon.BedrockAgent.Model.FunctionSchema ToFunctionSchema(this Kernel kernel) + { + var plugins = kernel.Plugins; + List functions = []; + foreach (var plugin in plugins) + { + foreach (KernelFunction function in plugin) + { + functions.Add(new Function + { + Name = function.Name, + Description = function.Description, + Parameters = function.Metadata.Parameters.CreateParameterSpec(), + // This field controls whether user confirmation is required to invoke the function. + // If this is set to "ENABLED", the user will be prompted to confirm the function invocation. + // Only after the user confirms, the function call request will be issued by the agent. + // If the user denies the confirmation, the agent will act as if the function does not exist. + // Currently, we do not support this feature, so we set it to "DISABLED". + RequireConfirmation = Amazon.BedrockAgent.RequireConfirmation.DISABLED, + }); + } + } + + return new Amazon.BedrockAgent.Model.FunctionSchema + { + Functions = functions, + }; + } + + private static Dictionary CreateParameterSpec( + this IReadOnlyList parameters) + { + Dictionary parameterSpec = []; + foreach (var parameter in parameters) + { + parameterSpec.Add(parameter.Name, new Amazon.BedrockAgent.Model.ParameterDetail + { + Description = parameter.Description, + Required = parameter.IsRequired, + Type = parameter.ParameterType.ToAmazonType(), + }); + } + + return parameterSpec; + } + + private static Amazon.BedrockAgent.Type ToAmazonType(this System.Type? parameterType) + { + var typeString = parameterType?.GetFriendlyTypeName(); + return typeString switch + { + "String" => Amazon.BedrockAgent.Type.String, + "Boolean" => Amazon.BedrockAgent.Type.Boolean, + "Int16" => Amazon.BedrockAgent.Type.Integer, + "UInt16" => Amazon.BedrockAgent.Type.Integer, + "Int32" => Amazon.BedrockAgent.Type.Integer, + "UInt32" => Amazon.BedrockAgent.Type.Integer, + "Int64" => Amazon.BedrockAgent.Type.Integer, + "UInt64" => Amazon.BedrockAgent.Type.Integer, + "Single" => Amazon.BedrockAgent.Type.Number, + "Double" => Amazon.BedrockAgent.Type.Number, + "Decimal" => Amazon.BedrockAgent.Type.Number, + "String[]" => Amazon.BedrockAgent.Type.Array, + "Boolean[]" => Amazon.BedrockAgent.Type.Array, + "Int16[]" => Amazon.BedrockAgent.Type.Array, + "UInt16[]" => Amazon.BedrockAgent.Type.Array, + "Int32[]" => Amazon.BedrockAgent.Type.Array, + "UInt32[]" => Amazon.BedrockAgent.Type.Array, + "Int64[]" => Amazon.BedrockAgent.Type.Array, + "UInt64[]" => Amazon.BedrockAgent.Type.Array, + "Single[]" => Amazon.BedrockAgent.Type.Array, + "Double[]" => Amazon.BedrockAgent.Type.Array, + "Decimal[]" => Amazon.BedrockAgent.Type.Array, + _ => throw new ArgumentException($"Unsupported parameter type: {typeString}"), + }; + } +} diff --git a/dotnet/src/Agents/Bedrock/Properties/AssemblyInfo.cs b/dotnet/src/Agents/Bedrock/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..bd1c0f58314e --- /dev/null +++ b/dotnet/src/Agents/Bedrock/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +// This assembly is currently experimental. +[assembly: Experimental("SKEXP0110")] diff --git a/dotnet/src/Agents/Bedrock/README.md b/dotnet/src/Agents/Bedrock/README.md new file mode 100644 index 000000000000..d480985fc667 --- /dev/null +++ b/dotnet/src/Agents/Bedrock/README.md @@ -0,0 +1,27 @@ +# Amazon Bedrock AI Agents in Semantic Kernel + +## Overview + +AWS Bedrock Agents is a managed service that allows users to stand up and run AI agents in the AWS cloud quickly. + +## Tools/Functions + +Bedrock Agents allow the use of tools via [action groups](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-action-create.html). + +The integration of Bedrock Agents with Semantic Kernel allows users to register kernel functions as tools in Bedrock Agents. + +## Enable code interpretation + +Bedrock Agents can write and execute code via a feature known as [code interpretation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-code-interpretation.html) similar to what OpenAI also offers. + +## Enable user input + +Bedrock Agents can [request user input](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-user-input.html) in case of missing information to invoke a tool. When this is enabled, the agent will prompt the user for the missing information. When this is disabled, the agent will guess the missing information. + +## Knowledge base + +Bedrock Agents can leverage data saved on AWS to perform RAG tasks, this is referred to as the [knowledge base](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-kb-add.html) in AWS. + +## Multi-agent + +Bedrock Agents support [multi-agent workflows](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-multi-agent-collaboration.html) for more complex tasks. However, it employs a different pattern than what we have in Semantic Kernel, thus this is not supported in the current integration. diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index dee4aed044c3..4d3d48c7acaa 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -47,4 +47,4 @@ - + \ No newline at end of file diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseBedrockAgentTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseBedrockAgentTest.cs new file mode 100644 index 000000000000..0a41c9c5778c --- /dev/null +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseBedrockAgentTest.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Amazon.BedrockAgent; +using Amazon.BedrockAgent.Model; +using Microsoft.SemanticKernel.Agents.Bedrock; + +/// +/// Base class for samples that demonstrate the usage of AWS Bedrock agents. +/// +public abstract class BaseBedrockAgentTest : BaseTest +{ + protected const string AgentDescription = "A helpful assistant who helps users find information."; + protected const string AgentInstruction = "You're a helpful assistant who helps users find information."; + protected readonly AmazonBedrockAgentClient Client; + + protected BaseBedrockAgentTest(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true) + { + Client = new AmazonBedrockAgentClient(); + } + + protected CreateAgentRequest GetCreateAgentRequest(string agentName) => new() + { + AgentName = agentName, + Description = AgentDescription, + Instruction = AgentInstruction, + AgentResourceRoleArn = TestConfiguration.BedrockAgent.AgentResourceRoleArn, + FoundationModel = TestConfiguration.BedrockAgent.FoundationModel, + }; + + protected override void Dispose(bool disposing) + { + Client?.Dispose(); + base.Dispose(disposing); + } + + /// + /// Override this method to create an agent with desired settings. + /// + /// The name of the agent to create. Must be unique. + protected abstract Task CreateAgentAsync(string agentName); +} diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs index 18809cac87f1..e45f52216a14 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs @@ -49,8 +49,8 @@ public static void Initialize(IConfigurationRoot configRoot) public static VertexAIConfig VertexAI => LoadSection(); public static AzureCosmosDbMongoDbConfig AzureCosmosDbMongoDb => LoadSection(); public static ApplicationInsightsConfig ApplicationInsights => LoadSection(); - public static CrewAIConfig CrewAI => LoadSection(); + public static BedrockAgentConfig BedrockAgent => LoadSection(); private static T LoadSection([CallerMemberName] string? caller = null) { @@ -323,4 +323,10 @@ public class CrewAIConfig public string Endpoint { get; set; } public string AuthToken { get; set; } } + + public class BedrockAgentConfig + { + public string AgentResourceRoleArn { get; set; } + public string FoundationModel { get; set; } + } } diff --git a/dotnet/src/SemanticKernel.Core/Contents/BinaryContentExtensions.cs b/dotnet/src/SemanticKernel.Core/Contents/BinaryContentExtensions.cs new file mode 100644 index 000000000000..f0d8b29ae280 --- /dev/null +++ b/dotnet/src/SemanticKernel.Core/Contents/BinaryContentExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; + +namespace Microsoft.SemanticKernel; + +/// +/// Provides extension methods for interacting with . +/// +public static class BinaryContentExtensions +{ + /// + /// Writes the content to a file. + /// + /// The content to write. + /// The path to the file to write to. + /// Whether to overwrite the file if it already exists. + public static void WriteToFile(this BinaryContent content, string filePath, bool overwrite = false) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentException("File path cannot be null or empty", nameof(filePath)); + } + + if (!overwrite && File.Exists(filePath)) + { + throw new InvalidOperationException("File already exists."); + } + + if (!content.CanRead) + { + throw new InvalidOperationException("No content to write to file."); + } + + File.WriteAllBytes(filePath, content.Data!.Value.ToArray()); + } +} diff --git a/python/samples/concepts/agents/bedrock_agent/README.md b/python/samples/concepts/agents/bedrock_agent/README.md index 0255aa561291..2e759a18f919 100644 --- a/python/samples/concepts/agents/bedrock_agent/README.md +++ b/python/samples/concepts/agents/bedrock_agent/README.md @@ -41,5 +41,5 @@ You need to make sure you have permission to access the foundation model. You ca 5. Under the `Permissions defined in this policy` section, click on the service. You should see **Bedrock** if you already have access to the Bedrock agent service. 6. Click on the service, and then click `Edit`. 7. On the right, you will be able to add an action. Find the service and search for `InvokeModelWithResponseStream`. -8. Check the box next to the action and then sroll all the way down and click `Next`. -9. Follow the prompts to save the changes. \ No newline at end of file +8. Check the box next to the action and then scroll all the way down and click `Next`. +9. Follow the prompts to save the changes.