diff --git a/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Settings/BedrockTextGenerationModelExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Settings/BedrockTextGenerationModelExecutionSettingsTests.cs index 0bd0051c38f7..bee77a94635d 100644 --- a/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Settings/BedrockTextGenerationModelExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Settings/BedrockTextGenerationModelExecutionSettingsTests.cs @@ -11,6 +11,7 @@ using Amazon.BedrockRuntime.Model; using Amazon.Runtime.Endpoints; using Microsoft.SemanticKernel.Connectors.Amazon.Core; +using Microsoft.SemanticKernel.Connectors.Amazon.Core; using Microsoft.SemanticKernel.TextGeneration; using Moq; using Xunit; diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockServiceFactory.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockServiceFactory.cs index e5b59f0c465a..fbc42ce690cd 100644 --- a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockServiceFactory.cs +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockServiceFactory.cs @@ -34,7 +34,12 @@ internal IBedrockTextGenerationService CreateTextGenerationService(string modelI case "AMAZON": if (modelName.StartsWith("titan-", StringComparison.OrdinalIgnoreCase)) { - return new AmazonService(); + return new AmazonTitanService(); + } + + if (modelName.StartsWith("nova-", StringComparison.OrdinalIgnoreCase)) + { + return new AmazonNovaService(); } throw new NotSupportedException($"Unsupported Amazon model: {modelId}"); case "ANTHROPIC": @@ -92,7 +97,11 @@ internal IBedrockChatCompletionService CreateChatCompletionService(string modelI case "AMAZON": if (modelName.StartsWith("titan-", StringComparison.OrdinalIgnoreCase)) { - return new AmazonService(); + return new AmazonTitanService(); + } + if (modelName.StartsWith("nova-", StringComparison.OrdinalIgnoreCase)) + { + return new AmazonNovaService(); } throw new NotSupportedException($"Unsupported Amazon model: {modelId}"); case "ANTHROPIC": diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/AmazonNovaService.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/AmazonNovaService.cs new file mode 100644 index 000000000000..a59df64daeae --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/AmazonNovaService.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using Amazon.BedrockRuntime.Model; +using Amazon.Runtime.Documents; +using Microsoft.SemanticKernel.ChatCompletion; +using static Microsoft.SemanticKernel.Connectors.Amazon.Core.NovaRequest; + +namespace Microsoft.SemanticKernel.Connectors.Amazon.Core; + +internal sealed class AmazonNovaService : IBedrockTextGenerationService, IBedrockChatCompletionService +{ + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() + { PropertyNameCaseInsensitive = true }; + + ConverseRequest IBedrockChatCompletionService.GetConverseRequest(string modelId, ChatHistory chatHistory, PromptExecutionSettings? settings) + { + var messages = BedrockModelUtilities.BuildMessageList(chatHistory); + var systemMessages = BedrockModelUtilities.GetSystemMessages(chatHistory); + + var executionSettings = AmazonNovaExecutionSettings.FromExecutionSettings(settings); + var schemaVersion = BedrockModelUtilities.GetExtensionDataValue(settings?.ExtensionData, "schemaVersion") ?? executionSettings.SchemaVersion; + var maxNewTokens = BedrockModelUtilities.GetExtensionDataValue(settings?.ExtensionData, "max_new_tokens") ?? executionSettings.MaxNewTokens; + var topP = BedrockModelUtilities.GetExtensionDataValue(settings?.ExtensionData, "top_p") ?? executionSettings.TopP; + var topK = BedrockModelUtilities.GetExtensionDataValue(settings?.ExtensionData, "top_k") ?? executionSettings.TopK; + var temperature = BedrockModelUtilities.GetExtensionDataValue(settings?.ExtensionData, "temperature") ?? executionSettings.Temperature; + var stopSequences = BedrockModelUtilities.GetExtensionDataValue?>(settings?.ExtensionData, "stopSequences") ?? executionSettings.StopSequences; + + var inferenceConfig = new InferenceConfiguration(); + BedrockModelUtilities.SetPropertyIfNotNull(() => temperature, value => inferenceConfig.Temperature = value); + BedrockModelUtilities.SetPropertyIfNotNull(() => topP, value => inferenceConfig.TopP = value); + BedrockModelUtilities.SetPropertyIfNotNull(() => maxNewTokens, value => inferenceConfig.MaxTokens = value); + BedrockModelUtilities.SetNullablePropertyIfNotNull(() => stopSequences, value => inferenceConfig.StopSequences = value); + + var converseRequest = new ConverseRequest + { + ModelId = modelId, + Messages = messages, + System = systemMessages, + InferenceConfig = inferenceConfig, + AdditionalModelRequestFields = new Document(), + AdditionalModelResponseFieldPaths = new List() + }; + + return converseRequest; + } + + ConverseStreamRequest IBedrockChatCompletionService.GetConverseStreamRequest(string modelId, ChatHistory chatHistory, PromptExecutionSettings? settings) + { + throw new System.NotImplementedException(); + } + + object IBedrockTextGenerationService.GetInvokeModelRequestBody(string modelId, string prompt, PromptExecutionSettings? executionSettings) + { + var settings = AmazonNovaExecutionSettings.FromExecutionSettings(executionSettings); + var schemaVersion = BedrockModelUtilities.GetExtensionDataValue(executionSettings?.ExtensionData, "schemaVersion") ?? settings.SchemaVersion; + var maxNewTokens = BedrockModelUtilities.GetExtensionDataValue(executionSettings?.ExtensionData, "max_new_tokens") ?? settings.MaxNewTokens; + var topP = BedrockModelUtilities.GetExtensionDataValue(executionSettings?.ExtensionData, "top_p") ?? settings.TopP; + var topK = BedrockModelUtilities.GetExtensionDataValue(executionSettings?.ExtensionData, "top_k") ?? settings.TopK; + var temperature = BedrockModelUtilities.GetExtensionDataValue(executionSettings?.ExtensionData, "temperature") ?? settings.Temperature; + var stopSequences = BedrockModelUtilities.GetExtensionDataValue?>(executionSettings?.ExtensionData, "stopSequences") ?? settings.StopSequences; + + var requestBody = new NovaRequest.NovaTextGenerationRequest() + { + InferenceConfig = new NovaRequest.NovaTextGenerationConfig + { + MaxNewTokens = maxNewTokens, + Temperature = temperature, + TopK = topK, + TopP = topP + }, + Messages = new List { new() { Role = AuthorRole.User.Label, Content = new List { new() { Text = prompt } } } }, + SchemaVersion = schemaVersion ?? "messages-v1", + }; + return requestBody; + } + + IReadOnlyList IBedrockTextGenerationService.GetInvokeResponseBody(InvokeModelResponse response) + { + using var reader = new StreamReader(response.Body); + var responseBody = JsonSerializer.Deserialize(reader.ReadToEnd(), s_jsonSerializerOptions); + List textContents = []; + if (responseBody?.Output?.Message?.Contents is not { Count: > 0 }) + { + return textContents; + } + string? outputText = responseBody.Output.Message.Contents[0].Text; + return [new TextContent(outputText, innerContent: responseBody)]; + } + + IEnumerable IBedrockTextGenerationService.GetTextStreamOutput(JsonNode chunk) + { + var text = chunk["output"]?["message"]?["content"]?["text"]?.ToString(); + if (!string.IsNullOrEmpty(text)) + { + yield return new StreamingTextContent(text, innerContent: chunk)!; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/NovaRequest.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/NovaRequest.cs new file mode 100644 index 000000000000..c75f5bd446ed --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/NovaRequest.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Amazon.Core; + +internal static class NovaRequest +{ + /// + /// The Nova Text Generation Request object. + /// + internal sealed class NovaTextGenerationRequest + { + /// + /// Schema version for the request, defaulting to "messages-v1". + /// + [JsonPropertyName("schemaVersion")] + public string SchemaVersion { get; set; } = "messages-v1"; + + /// + /// System messages providing context for the generation. + /// + [JsonPropertyName("system")] + public IList? System { get; set; } + + /// + /// User messages for text generation. + /// + [JsonPropertyName("messages")] + public IList? Messages { get; set; } + + /// + /// Text generation configurations as required by Nova request body. + /// + [JsonPropertyName("inferenceConfig")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public NovaTextGenerationConfig? InferenceConfig { get; set; } + } + + /// + /// Represents a system message. + /// + internal sealed class NovaSystemMessage + { + /// + /// The text of the system message. + /// + [JsonPropertyName("text")] + public string? Text { get; set; } + } + + /// + /// Represents a user message. + /// + internal sealed class NovaUserMessage + { + /// + /// The role of the message sender. + /// + [JsonPropertyName("role")] + public string? Role { get; set; } + + /// + /// The content of the user message. + /// + [JsonPropertyName("content")] + public IList? Content { get; set; } = new List(); + } + + /// + /// Represents the content of a user message. + /// + internal sealed class NovaUserMessageContent + { + /// + /// The text of the user message content. + /// + [JsonPropertyName("text")] + public string? Text { get; set; } + } + + /// + /// Nova Text Generation Configurations. + /// + internal sealed class NovaTextGenerationConfig + { + /// + /// Maximum new tokens to generate in the response. + /// + [JsonPropertyName("max_new_tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxNewTokens { get; set; } + + /// + /// Top P controls token choices, based on the probability of the potential choices. The range is 0 to 1. The default is 1. + /// + [JsonPropertyName("top_p")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? TopP { get; set; } + + /// + /// Top K limits the number of token options considered at each generation step. The default is 20. + /// + [JsonPropertyName("top_k")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TopK { get; set; } + + /// + /// The Temperature value ranges from 0 to 1, with 0 being the most deterministic and 1 being the most creative. + /// + [JsonPropertyName("temperature")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Temperature { get; set; } + } +} diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/NovaResponse.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/NovaResponse.cs new file mode 100644 index 000000000000..e1630a309eb0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Nova/NovaResponse.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Amazon.Core; + +internal sealed class NovaMessage +{ + internal sealed class Content + { + public string? Text { get; set; } + } + + [JsonPropertyName("content")] + public List? Contents { get; set; } + + public string? Role { get; set; } +} + +internal sealed class Output +{ + public NovaMessage? Message { get; set; } +} + +internal sealed class Usage +{ + public int InputTokens { get; set; } + + public int OutputTokens { get; set; } + + public int TotalTokens { get; set; } +} + +/// +/// The Amazon Titan Text response object when deserialized from Invoke Model call. +/// +internal sealed class NovaTextResponse +{ + public Output? Output { get; set; } + + public Usage? Usage { get; set; } + + public string? StopReason { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/AmazonService.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/AmazonTitanService.cs similarity index 98% rename from dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/AmazonService.cs rename to dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/AmazonTitanService.cs index cd1d82a255dc..c31bee6781a4 100644 --- a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/AmazonService.cs +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/AmazonTitanService.cs @@ -13,7 +13,7 @@ namespace Microsoft.SemanticKernel.Connectors.Amazon.Core; /// /// Input-output service for Amazon Titan model. /// -internal sealed class AmazonService : IBedrockTextGenerationService, IBedrockChatCompletionService +internal sealed class AmazonTitanService : IBedrockTextGenerationService, IBedrockChatCompletionService { /// public object GetInvokeModelRequestBody(string modelId, string prompt, PromptExecutionSettings? executionSettings) diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/TitanRequest.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/TitanRequest.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/TitanRequest.cs rename to dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/TitanRequest.cs diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/TitanResponse.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/TitanResponse.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/TitanResponse.cs rename to dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Models/Amazon/Titan/TitanResponse.cs diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Settings/AmazonNovaExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Settings/AmazonNovaExecutionSettings.cs new file mode 100644 index 000000000000..010611ebe4ff --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Settings/AmazonNovaExecutionSettings.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.Connectors.Amazon; + +/// +/// Prompt execution settings for Nova Text Generation +/// +[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] +public class AmazonNovaExecutionSettings : PromptExecutionSettings +{ + private string? _schemaVersion; + private int? _maxNewTokens; + private float? _topP; + private int? _topK; + private float? _temperature; + private List? _stopSequences; + + /// + /// Schema version for the execution settings. + /// + [JsonPropertyName("schemaVersion")] + public string? SchemaVersion + { + get => this._schemaVersion; + set + { + this.ThrowIfFrozen(); + this._schemaVersion = value; + } + } + + /// + /// Maximum new tokens to generate in the response. + /// + [JsonPropertyName("max_new_tokens")] + public int? MaxNewTokens + { + get => this._maxNewTokens; + set + { + this.ThrowIfFrozen(); + this._maxNewTokens = value; + } + } + + /// + /// Top P controls token choices, based on the probability of the potential choices. The range is 0 to 1. The default is 1. + /// + [JsonPropertyName("top_p")] + public float? TopP + { + get => this._topP; + set + { + this.ThrowIfFrozen(); + this._topP = value; + } + } + + /// + /// Top K limits the number of token options considered at each generation step. The default is 20. + /// + [JsonPropertyName("top_k")] + public int? TopK + { + get => this._topK; + set + { + this.ThrowIfFrozen(); + this._topK = value; + } + } + + /// + /// The Temperature value ranges from 0 to 1, with 0 being the most deterministic and 1 being the most creative. + /// + [JsonPropertyName("temperature")] + public float? Temperature + { + get => this._temperature; + set + { + this.ThrowIfFrozen(); + this._temperature = value; + } + } + + /// + /// Use | (pipe) characters (maximum 20 characters). + /// + [JsonPropertyName("stopSequences")] + public List? StopSequences + { + get => this._stopSequences; + set + { + this.ThrowIfFrozen(); + this._stopSequences = value; + } + } + + /// + /// Converts PromptExecutionSettings to NovaExecutionSettings + /// + /// The Kernel standard PromptExecutionSettings. + /// Model specific execution settings + public static AmazonNovaExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings) + { + switch (executionSettings) + { + case null: + return new AmazonNovaExecutionSettings(); + case AmazonNovaExecutionSettings settings: + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + return JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive)!; + } +} diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/ActivityExtensions.cs b/dotnet/src/InternalUtilities/src/Diagnostics/ActivityExtensions.cs index 6b21ce027628..ebbf57badea9 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/ActivityExtensions.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/ActivityExtensions.cs @@ -27,7 +27,7 @@ public static Activity SetTags(this Activity activity, ReadOnlySpan