From 8f8170d0c4af8b66487ec1f8c35a3cc15196b91a Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Wed, 3 Jul 2024 21:08:59 +0200 Subject: [PATCH 01/19] Implemented chat generation (non streaming) --- .../Core/AnthropicClient.cs | 184 +++++++++++++++++- .../Core/Models/AnthropicRequest.cs | 12 +- .../Core/Models/AnthropicResponse.cs | 41 ++++ .../AnthropicToolFunctionDeclaration.cs | 2 +- .../Core/Models/Message/AnthropicContent.cs | 4 +- .../Message/AnthropicJsonDeltaContent.cs | 21 ++ .../Models/Message/AnthropicTextContent.cs | 1 + .../Models/AnthropicFinishReason.cs | 7 +- .../Models/AnthropicFunction.cs | 12 +- .../Models/AnthropicFunctionToolCall.cs | 2 +- .../Models/AnthropicMetadata.cs | 6 + .../Models/AnthropicUsage.cs | 30 +++ 12 files changed, 305 insertions(+), 17 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index a73783c4d942..acb0c1f3411e 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Text.Json; @@ -10,6 +12,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; @@ -19,6 +22,8 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// internal sealed class AnthropicClient { + private const string ModelProvider = "anthropic"; + private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly string _modelId; @@ -27,6 +32,40 @@ internal sealed class AnthropicClient private readonly Func? _customRequestHandler; private readonly AnthropicClientOptions _options; + private static readonly string s_namespace = typeof(AnthropicChatCompletionService).Namespace!; + + /// + /// Instance of for metrics. + /// + private static readonly Meter s_meter = new(s_namespace); + + /// + /// Instance of to keep track of the number of prompt tokens used. + /// + private static readonly Counter s_promptTokensCounter = + s_meter.CreateCounter( + name: $"{s_namespace}.tokens.prompt", + unit: "{token}", + description: "Number of prompt tokens used"); + + /// + /// Instance of to keep track of the number of completion tokens used. + /// + private static readonly Counter s_completionTokensCounter = + s_meter.CreateCounter( + name: $"{s_namespace}.tokens.completion", + unit: "{token}", + description: "Number of completion tokens used"); + + /// + /// Instance of to keep track of the total number of tokens used. + /// + private static readonly Counter s_totalTokensCounter = + s_meter.CreateCounter( + name: $"{s_namespace}.tokens.total", + unit: "{token}", + description: "Number of tokens used"); + /// /// Represents a client for interacting with the Anthropic chat completion models. /// @@ -97,8 +136,131 @@ public async Task> GenerateChatMessageAsync( Kernel? kernel = null, CancellationToken cancellationToken = default) { - await Task.Yield(); - throw new NotImplementedException("Implement this method in next PR."); + var state = this.ValidateInputAndCreateChatCompletionState(chatHistory, executionSettings); + + using var activity = ModelDiagnostics.StartCompletionActivity( + this._endpoint, this._modelId, ModelProvider, chatHistory, state.ExecutionSettings); + + List chatResponses; + AnthropicResponse anthropicResponse; + try + { + anthropicResponse = await this.SendRequestAndReturnValidResponseAsync( + this._endpoint, state.AnthropicRequest, cancellationToken) + .ConfigureAwait(false); + chatResponses = this.GetChatResponseFrom(anthropicResponse); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + activity?.SetCompletionResponse( + chatResponses, + anthropicResponse.Usage?.InputTokens, + anthropicResponse.Usage?.OutputTokens); + + return chatResponses; + } + + private List GetChatResponseFrom(AnthropicResponse response) + { + var chatMessageContents = this.GetChatMessageContentsFromResponse(response); + this.LogUsage(chatMessageContents); + return chatMessageContents; + } + + private void LogUsage(List chatMessageContents) + { + if (chatMessageContents[0].Metadata is not AnthropicMetadata { TotalTokenCount: > 0 } metadata) + { + this.Log(LogLevel.Debug, "Token usage information unavailable."); + return; + } + + this.Log(LogLevel.Information, + "Prompt tokens: {PromptTokens}. Completion tokens: {CompletionTokens}. Total tokens: {TotalTokens}.", + metadata.InputTokenCount, + metadata.OutputTokenCount, + metadata.TotalTokenCount); + + s_promptTokensCounter.Add(metadata.InputTokenCount); + s_completionTokensCounter.Add(metadata.OutputTokenCount); + s_totalTokensCounter.Add(metadata.TotalTokenCount); + } + + private List GetChatMessageContentsFromResponse(AnthropicResponse response) + => response.Contents!.Select(content => this.GetChatMessageContentFromAnthropicContent(response, content)).ToList(); + + private ChatMessageContent GetChatMessageContentFromAnthropicContent(AnthropicResponse response, AnthropicContent content) + { + if (content is not AnthropicTextContent textContent) + { + throw new NotSupportedException($"Content type {content.GetType()} is not supported yet."); + } + + return new ChatMessageContent( + role: response.Role, + content: textContent.Text ?? string.Empty, + modelId: response.ModelId ?? this._modelId, + metadata: GetResponseMetadata(response)); + } + + private static AnthropicMetadata GetResponseMetadata(AnthropicResponse response) + => new() + { + MessageId = response.Id, + FinishReason = response.StopReason, + StopSequence = response.StopSequence, + InputTokenCount = response.Usage?.InputTokens ?? 0, + OutputTokenCount = response.Usage?.OutputTokens ?? 0 + }; + + private async Task SendRequestAndReturnValidResponseAsync( + Uri endpoint, + AnthropicRequest anthropicRequest, + CancellationToken cancellationToken) + { + using var httpRequestMessage = await this.CreateHttpRequestAsync(anthropicRequest, endpoint).ConfigureAwait(false); + string body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken) + .ConfigureAwait(false); + var response = DeserializeResponse(body); + ValidateAnthropicResponse(response); + return response; + } + + private static void ValidateAnthropicResponse(AnthropicResponse response) + { + if (response.Contents is null || response.Contents.Count == 0) + { + throw new KernelException("Anthropic API doesn't return any data."); + } + } + + private ChatCompletionState ValidateInputAndCreateChatCompletionState( + ChatHistory chatHistory, + PromptExecutionSettings? executionSettings) + { + ValidateChatHistory(chatHistory); + + var anthropicExecutionSettings = AnthropicPromptExecutionSettings.FromExecutionSettings(executionSettings); + ValidateMaxTokens(anthropicExecutionSettings.MaxTokens); + + this.Log(LogLevel.Trace, "ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chatHistory), + JsonSerializer.Serialize(anthropicExecutionSettings)); + + var filteredChatHistory = new ChatHistory(chatHistory.Where(IsAssistantOrUserOrSystem)); + return new ChatCompletionState() + { + ChatHistory = chatHistory, + ExecutionSettings = anthropicExecutionSettings, + AnthropicRequest = AnthropicRequest.FromChatHistoryAndExecutionSettings(filteredChatHistory, anthropicExecutionSettings) + }; + + static bool IsAssistantOrUserOrSystem(ChatMessageContent msg) + => msg.Role == AuthorRole.Assistant || msg.Role == AuthorRole.User || msg.Role == AuthorRole.System; } /// @@ -129,6 +291,15 @@ private static void ValidateMaxTokens(int? maxTokens) } } + private static void ValidateChatHistory(ChatHistory chatHistory) + { + Verify.NotNullOrEmpty(chatHistory); + if (chatHistory.All(msg => msg.Role == AuthorRole.System)) + { + throw new InvalidOperationException("Chat history can't contain only system messages."); + } + } + private async Task SendRequestAndGetStringBodyAsync( HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) @@ -179,7 +350,7 @@ private async Task CreateHttpRequestAsync(object requestData return httpRequestMessage; } - private void Log(LogLevel logLevel, string? message, params object[] args) + private void Log(LogLevel logLevel, string? message, params object?[] args) { if (this._logger.IsEnabled(logLevel)) { @@ -188,4 +359,11 @@ private void Log(LogLevel logLevel, string? message, params object[] args) #pragma warning restore CA2254 } } + + private sealed class ChatCompletionState + { + internal ChatHistory ChatHistory { get; set; } = null!; + internal AnthropicRequest AnthropicRequest { get; set; } = null!; + internal AnthropicPromptExecutionSettings ExecutionSettings { get; set; } = null!; + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 3f65e8ca2e95..9b987019b99e 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -107,11 +107,11 @@ internal static AnthropicRequest FromChatHistoryAndExecutionSettings( bool streamingMode = false) { AnthropicRequest request = CreateRequest(chatHistory, executionSettings, streamingMode); - AddMessages(chatHistory, request); + AddMessages(chatHistory.Where(msg => msg.Role != AuthorRole.System), request); return request; } - private static void AddMessages(ChatHistory chatHistory, AnthropicRequest request) + private static void AddMessages(IEnumerable chatHistory, AnthropicRequest request) => request.Messages = chatHistory.Select(CreateClaudeMessageFromChatMessage).ToList(); private static Message CreateClaudeMessageFromChatMessage(ChatMessageContent message) @@ -129,7 +129,11 @@ private static AnthropicRequest CreateRequest(ChatHistory chatHistory, Anthropic { ModelId = executionSettings.ModelId ?? throw new InvalidOperationException("Model ID must be provided."), MaxTokens = executionSettings.MaxTokens ?? throw new InvalidOperationException("Max tokens must be provided."), - SystemPrompt = chatHistory.SingleOrDefault(c => c.Role == AuthorRole.System)?.Content, + SystemPrompt = string.Join("\n", chatHistory + .Where(msg => msg.Role == AuthorRole.System) + .SelectMany(msg => msg.Items) + .OfType() + .Select(content => content.Text)), StopSequences = executionSettings.StopSequences, Stream = streamingMode, Temperature = executionSettings.Temperature, @@ -141,7 +145,7 @@ private static AnthropicRequest CreateRequest(ChatHistory chatHistory, Anthropic private static List CreateClaudeMessages(ChatMessageContent content) { - List messages = new(); + List messages = []; switch (content) { case AnthropicChatMessageContent { CalledToolResult: not null } contentWithCalledTool: diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs new file mode 100644 index 000000000000..517717b81e3d --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +internal sealed class AnthropicResponse +{ + [JsonRequired] + [JsonPropertyName("id")] + public string Id { get; init; } = null!; + + [JsonRequired] + [JsonPropertyName("type")] + public string Type { get; init; } = null!; + + [JsonRequired] + [JsonPropertyName("role")] + [JsonConverter(typeof(AuthorRoleConverter))] + public AuthorRole Role { get; init; } + + [JsonRequired] + [JsonPropertyName("content")] + public IReadOnlyList Contents { get; init; } = null!; + + [JsonRequired] + [JsonPropertyName("model")] + public string ModelId { get; init; } = null!; + + [JsonPropertyName("stop_reason")] + public AnthropicFinishReason? StopReason { get; init; } + + [JsonPropertyName("stop_sequence")] + public string? StopSequence { get; init; } + + [JsonRequired] + [JsonPropertyName("usage")] + public AnthropicUsage Usage { get; init; } = null!; +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs index abfbbad17779..f16cdb73274e 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs @@ -7,7 +7,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// /// A Tool is a piece of code that enables the system to interact with external systems to perform an action, -/// or set of actions, outside of knowledge and scope of the model. +/// or set of actions, outside the knowledge and scope of the model. /// Structured representation of a function declaration as defined by the OpenAPI 3.03 specification. /// Included in this declaration are the function name and parameters. /// This FunctionDeclaration is a representation of a block of code that can be used as a Tool by the model and executed by the client. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index c27931519b16..97b083dede66 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -9,7 +9,9 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] [JsonDerivedType(typeof(AnthropicTextContent), typeDiscriminator: "text")] +[JsonDerivedType(typeof(AnthropicTextContent), typeDiscriminator: "text_delta")] +[JsonDerivedType(typeof(AnthropicJsonDeltaContent), typeDiscriminator: "input_json_delta")] [JsonDerivedType(typeof(AnthropicImageContent), typeDiscriminator: "image")] [JsonDerivedType(typeof(AnthropicToolCallContent), typeDiscriminator: "tool_use")] [JsonDerivedType(typeof(AnthropicToolResultContent), typeDiscriminator: "tool_result")] -internal abstract class AnthropicContent { } +internal abstract class AnthropicContent; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs new file mode 100644 index 000000000000..a068d612bcd7 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +internal sealed class AnthropicJsonDeltaContent +{ + [JsonConstructor] + public AnthropicJsonDeltaContent(string partialJson) + { + this.PartialJson = partialJson; + } + + /// + /// Only used when type is "input_json_delta". The partial json content. + /// + [JsonRequired] + [JsonPropertyName("partial_json")] + public string PartialJson { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs index ca565be761f6..80f3aea31caa 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs @@ -15,6 +15,7 @@ public AnthropicTextContent(string text) /// /// Only used when type is "text". The text content. /// + [JsonRequired] [JsonPropertyName("text")] public string Text { get; set; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs index d05f9bc69547..34c911b0ea78 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs @@ -7,7 +7,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// -/// Represents a Claude Finish Reason. +/// Represents a Anthropic Finish Reason. /// [JsonConverter(typeof(ClaudeFinishReasonConverter))] public readonly struct AnthropicFinishReason : IEquatable @@ -27,6 +27,11 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// public static AnthropicFinishReason StopSequence { get; } = new("stop_sequence"); + /// + /// The model invoked one or more tools + /// + public static AnthropicFinishReason ToolUse { get; } = new("tool_use"); + /// /// Gets the label of the property. /// Label is used for serialization. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs index 55ad7872a423..60896e39bff9 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs @@ -8,11 +8,11 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; // NOTE: Since this space is evolving rapidly, in order to reduce the risk of needing to take breaking -// changes as Gemini's APIs evolve, these types are not externally constructible. In the future, once +// changes as Anthropic's APIs evolve, these types are not externally constructible. In the future, once // things stabilize, and if need demonstrates, we could choose to expose those constructors. /// -/// Represents a function parameter that can be passed to an Gemini function tool call. +/// Represents a function parameter that can be passed to an Anthropic function tool call. /// public sealed class ClaudeFunctionParameter { @@ -47,7 +47,7 @@ internal ClaudeFunctionParameter( } /// -/// Represents a function return parameter that can be returned by a tool call to Gemini. +/// Represents a function return parameter that can be returned by a tool call to Anthropic. /// public sealed class ClaudeFunctionReturnParameter { @@ -72,7 +72,7 @@ internal ClaudeFunctionReturnParameter( } /// -/// Represents a function that can be passed to the Gemini API +/// Represents a function that can be passed to the Anthropic API /// public sealed class AnthropicFunction { @@ -99,7 +99,7 @@ internal AnthropicFunction( } /// Gets the separator used between the plugin name and the function name, if a plugin name is present. - /// Default is _
It can't be -, because Gemini truncates the plugin name if a dash is used
+ /// Default is _
It can't be -, because Anthropic truncates the plugin name if a dash is used
public static string NameSeparator { get; set; } = "_"; /// Gets the name of the plugin with which the function is associated, if any. @@ -127,7 +127,7 @@ internal AnthropicFunction( public ClaudeFunctionReturnParameter? ReturnParameter { get; } /// - /// Converts the representation to the Gemini API's + /// Converts the representation to the Anthropic API's /// representation. /// /// A containing all the function information. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs index 7ed158020e35..e59cb387ae2a 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// -/// Represents an Gemini function tool call with deserialized function name and arguments. +/// Represents an Anthropic function tool call with deserialized function name and arguments. /// public sealed class AnthropicFunctionToolCall { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs index 3cc73f27b658..d2a25ccf5ddf 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs @@ -61,6 +61,12 @@ public int OutputTokenCount internal init => this.SetValueInDictionary(value, nameof(this.OutputTokenCount)); } + /// + /// Represents the total count of tokens in the Anthropic response, + /// which is calculated by summing the input token count and the output token count. + /// + public int TotalTokenCount => this.InputTokenCount + this.OutputTokenCount; + /// /// Converts a dictionary to a object. /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs new file mode 100644 index 000000000000..b05356684b03 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic; + +/// +/// Billing and rate-limit usage.
+/// Anthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems.
+/// Under the hood, the API transforms requests into a format suitable for the model. +/// The model's output then goes through a parsing stage before becoming an API response. +/// As a result, the token counts in usage will not match one-to-one with the exact visible content of an API request or response.
+/// For example, OutputTokens will be non-zero, even for an empty string response from Claude. +///
+public sealed class AnthropicUsage +{ + /// + /// The number of input tokens which were used. + /// + [JsonRequired] + [JsonPropertyName("input_tokens")] + public int InputTokens { get; init; } + + /// + /// The number of output tokens which were used + /// + [JsonRequired] + [JsonPropertyName("output_tokens")] + public int OutputTokens { get; init; } +} From 5f20bfa8ea660ddb8e56a355603c9589cf9e41b8 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Wed, 3 Jul 2024 23:17:15 +0200 Subject: [PATCH 02/19] Removed FC from request --- .../Core/Models/AnthropicRequest.cs | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 9b987019b99e..4dfc04e15ca7 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -145,29 +145,7 @@ private static AnthropicRequest CreateRequest(ChatHistory chatHistory, Anthropic private static List CreateClaudeMessages(ChatMessageContent content) { - List messages = []; - switch (content) - { - case AnthropicChatMessageContent { CalledToolResult: not null } contentWithCalledTool: - messages.Add(new AnthropicToolResultContent - { - ToolId = contentWithCalledTool.CalledToolResult.ToolUseId ?? throw new InvalidOperationException("Tool ID must be provided."), - Content = new AnthropicTextContent(contentWithCalledTool.CalledToolResult.FunctionResult.ToString()) - }); - break; - case AnthropicChatMessageContent { ToolCalls: not null } contentWithToolCalls: - messages.AddRange(contentWithToolCalls.ToolCalls.Select(toolCall => - new AnthropicToolCallContent - { - ToolId = toolCall.ToolUseId, - FunctionName = toolCall.FullyQualifiedName, - Arguments = JsonSerializer.SerializeToNode(toolCall.Arguments), - })); - break; - default: - messages.AddRange(content.Items.Select(GetClaudeMessageFromKernelContent)); - break; - } + var messages = content.Items.Select(GetClaudeMessageFromKernelContent).ToList(); if (messages.Count == 0) { From e876983edbea7b8910486e8a024abd49027f5a28 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Wed, 3 Jul 2024 23:19:32 +0200 Subject: [PATCH 03/19] Added Unit tests and fixes --- .../AnthropicToolCallBehaviorTests.cs | 222 ---------- .../Core/AnthropicChatGenerationTests.cs | 416 ++++++++++++++++++ .../Core/AnthropicRequestTests.cs | 133 +----- .../Models/AnthropicFunctionTests.cs | 185 -------- .../Models/AnthropicFunctionToolCallTests.cs | 71 --- .../TestData/chat_one_response.json | 18 + ...thropicKernelFunctionMetadataExtensions.cs | 52 --- .../AnthropicClientOptions.cs | 10 +- .../AnthropicPromptExecutionSettings.cs | 39 -- .../AnthropicToolCallBehavior.cs | 228 ---------- .../Core/AnthropicClient.cs | 64 ++- .../Core/Models/AnthropicRequest.cs | 54 ++- .../Core/Models/AnthropicResponse.cs | 2 +- .../AnthropicToolFunctionDeclaration.cs | 40 -- .../Core/Models/Message/AnthropicContent.cs | 11 +- ...ontent.cs => AnthropicDeltaJsonContent.cs} | 8 +- .../Message/AnthropicDeltaTextContent.cs | 15 + .../Models/Message/AnthropicImageContent.cs | 14 - .../Models/Message/AnthropicTextContent.cs | 6 - .../Message/AnthropicToolCallContent.cs | 30 -- .../Message/AnthropicToolResultContent.cs | 19 - .../Models/Message/TypeJsonDyscriminator.cs | 121 +++++ .../AnthropicKernelBuilderExtensions.cs | 2 +- .../AnthropicServiceCollectionExtensions.cs | 2 +- .../Models/AnthropicChatMessageContent.cs | 73 +-- .../Models/AnthropicFunction.cs | 181 -------- .../Models/AnthropicFunctionToolCall.cs | 89 ---- .../Models/AnthropicFunctionToolResult.cs | 39 -- .../AnthropicChatCompletionService.cs | 2 +- 29 files changed, 697 insertions(+), 1449 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic.UnitTests/AnthropicToolCallBehaviorTests.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionToolCallTests.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic.UnitTests/TestData/chat_one_response.json delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Utils/AnthropicKernelFunctionMetadataExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/AnthropicToolCallBehavior.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs rename dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/{AnthropicJsonDeltaContent.cs => AnthropicDeltaJsonContent.cs} (67%) create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolCallContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolResultContent.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolResult.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/AnthropicToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/AnthropicToolCallBehaviorTests.cs deleted file mode 100644 index ed881a793c05..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/AnthropicToolCallBehaviorTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.Anthropic; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; -using Xunit; - -namespace SemanticKernel.Connectors.Anthropic.UnitTests; - -/// -/// Unit tests for -/// -public sealed class AnthropicToolCallBehaviorTests -{ - [Fact] - public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() - { - // Arrange & Act - var behavior = AnthropicToolCallBehavior.EnableKernelFunctions; - - // Assert - Assert.IsType(behavior); - Assert.Equal(0, behavior.MaximumAutoInvokeAttempts); - } - - [Fact] - public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() - { - // Arrange & Act - var behavior = AnthropicToolCallBehavior.AutoInvokeKernelFunctions; - - // Assert - Assert.IsType(behavior); - Assert.Equal(5, behavior.MaximumAutoInvokeAttempts); - } - - [Fact] - public void EnableFunctionsReturnsEnabledFunctionsInstance() - { - // Arrange & Act - List functions = - [new AnthropicFunction("Plugin", "Function", "description", [], null)]; - var behavior = AnthropicToolCallBehavior.EnableFunctions(functions); - - // Assert - Assert.IsType(behavior); - } - - [Fact] - public void KernelFunctionsConfigureClaudeRequestWithNullKernelDoesNotAddTools() - { - // Arrange - var kernelFunctions = new AnthropicToolCallBehavior.KernelFunctions(autoInvoke: false); - var claudeRequest = new AnthropicRequest(); - - // Act - kernelFunctions.ConfigureClaudeRequest(null, claudeRequest); - - // Assert - Assert.Null(claudeRequest.Tools); - } - - [Fact] - public void KernelFunctionsConfigureClaudeRequestWithoutFunctionsDoesNotAddTools() - { - // Arrange - var kernelFunctions = new AnthropicToolCallBehavior.KernelFunctions(autoInvoke: false); - var claudeRequest = new AnthropicRequest(); - var kernel = Kernel.CreateBuilder().Build(); - - // Act - kernelFunctions.ConfigureClaudeRequest(kernel, claudeRequest); - - // Assert - Assert.Null(claudeRequest.Tools); - } - - [Fact] - public void KernelFunctionsConfigureClaudeRequestWithFunctionsAddsTools() - { - // Arrange - var kernelFunctions = new AnthropicToolCallBehavior.KernelFunctions(autoInvoke: false); - var claudeRequest = new AnthropicRequest(); - var kernel = Kernel.CreateBuilder().Build(); - var plugin = GetTestPlugin(); - kernel.Plugins.Add(plugin); - - // Act - kernelFunctions.ConfigureClaudeRequest(kernel, claudeRequest); - - // Assert - AssertFunctions(claudeRequest); - } - - [Fact] - public void EnabledFunctionsConfigureClaudeRequestWithoutFunctionsDoesNotAddTools() - { - // Arrange - var enabledFunctions = new AnthropicToolCallBehavior.EnabledFunctions([], autoInvoke: false); - var claudeRequest = new AnthropicRequest(); - - // Act - enabledFunctions.ConfigureClaudeRequest(null, claudeRequest); - - // Assert - Assert.Null(claudeRequest.Tools); - } - - [Fact] - public void EnabledFunctionsConfigureClaudeRequestWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var functions = GetTestPlugin().GetFunctionsMetadata().Select(function => AnthropicKernelFunctionMetadataExtensions.ToClaudeFunction(function)); - var enabledFunctions = new AnthropicToolCallBehavior.EnabledFunctions(functions, autoInvoke: true); - var claudeRequest = new AnthropicRequest(); - - // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureClaudeRequest(null, claudeRequest)); - Assert.Equal( - $"Auto-invocation with {nameof(AnthropicToolCallBehavior.EnabledFunctions)} is not supported when no kernel is provided.", - exception.Message); - } - - [Fact] - public void EnabledFunctionsConfigureClaudeRequestWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var functions = GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToClaudeFunction()); - var enabledFunctions = new AnthropicToolCallBehavior.EnabledFunctions(functions, autoInvoke: true); - var claudeRequest = new AnthropicRequest(); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureClaudeRequest(kernel, claudeRequest)); - Assert.Equal( - $"The specified {nameof(AnthropicToolCallBehavior.EnabledFunctions)} function MyPlugin{AnthropicFunction.NameSeparator}MyFunction is not available in the kernel.", - exception.Message); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnabledFunctionsConfigureClaudeRequestWithKernelAndPluginsAddsTools(bool autoInvoke) - { - // Arrange - var plugin = GetTestPlugin(); - var functions = plugin.GetFunctionsMetadata().Select(function => function.ToClaudeFunction()); - var enabledFunctions = new AnthropicToolCallBehavior.EnabledFunctions(functions, autoInvoke); - var claudeRequest = new AnthropicRequest(); - var kernel = Kernel.CreateBuilder().Build(); - - kernel.Plugins.Add(plugin); - - // Act - enabledFunctions.ConfigureClaudeRequest(kernel, claudeRequest); - - // Assert - AssertFunctions(claudeRequest); - } - - [Fact] - public void EnabledFunctionsCloneReturnsCorrectClone() - { - // Arrange - var functions = GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToClaudeFunction()); - var toolcallbehavior = new AnthropicToolCallBehavior.EnabledFunctions(functions, autoInvoke: true); - - // Act - var clone = toolcallbehavior.Clone(); - - // Assert - Assert.IsType(clone); - Assert.NotSame(toolcallbehavior, clone); - Assert.Equivalent(toolcallbehavior, clone, strict: true); - } - - [Fact] - public void KernelFunctionsCloneReturnsCorrectClone() - { - // Arrange - var functions = GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToClaudeFunction()); - var toolcallbehavior = new AnthropicToolCallBehavior.KernelFunctions(autoInvoke: true); - - // Act - var clone = toolcallbehavior.Clone(); - - // Assert - Assert.IsType(clone); - Assert.NotSame(toolcallbehavior, clone); - Assert.Equivalent(toolcallbehavior, clone, strict: true); - } - - private static KernelPlugin GetTestPlugin() - { - var function = KernelFunctionFactory.CreateFromMethod( - (string parameter1, string parameter2) => "Result1", - "MyFunction", - "Test Function", - [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], - new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); - - return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - } - - private static void AssertFunctions(AnthropicRequest request) - { - Assert.NotNull(request.Tools); - Assert.Single(request.Tools); - - var function = request.Tools[0]; - - Assert.NotNull(function); - - Assert.Equal($"MyPlugin{AnthropicFunction.NameSeparator}MyFunction", function.Name); - Assert.Equal("Test Function", function.Description); - Assert.Equal("""{"type":"object","required":[],"properties":{"parameter1":{"type":"string"},"parameter2":{"type":"string"}}}""", - JsonSerializer.Serialize(function.Parameters)); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs new file mode 100644 index 000000000000..b6d5c70d6cfb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.Anthropic; +using Microsoft.SemanticKernel.Connectors.Anthropic.Core; +using Microsoft.SemanticKernel.Http; +using Xunit; + +namespace SemanticKernel.Connectors.Anthropic.UnitTests.Core; + +/// +/// Test for +/// +public sealed class AnthropicClientChatGenerationTests : IDisposable +{ + private readonly HttpClient _httpClient; + private readonly HttpMessageHandlerStub _messageHandlerStub; + private const string ChatTestDataFilePath = "./TestData/chat_one_response.json"; + + public AnthropicClientChatGenerationTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._messageHandlerStub.ResponseToReturn.Content = new StringContent( + File.ReadAllText(ChatTestDataFilePath)); + + this._httpClient = new HttpClient(this._messageHandlerStub, false); + } + + [Fact] + public async Task ShouldPassModelIdToRequestContentAsync() + { + // Arrange + string modelId = "fake-model234"; + var client = this.CreateChatCompletionClient(modelId: modelId); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicRequest? request = Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Contains(modelId, request.ModelId, StringComparison.Ordinal); + } + + [Fact] + public async Task ShouldContainRolesInRequestAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn.Content = new StringContent( + await File.ReadAllTextAsync(ChatTestDataFilePath)); + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicRequest? request = Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Collection(request.Messages, + item => Assert.Equal(chatHistory[1].Role, item.Role), + item => Assert.Equal(chatHistory[2].Role, item.Role), + item => Assert.Equal(chatHistory[3].Role, item.Role)); + } + + [Fact] + public async Task ShouldReturnValidChatResponseAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + var response = await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.NotNull(response); + Assert.Equal("Hi! My name is Claude.", response[0].Content); + Assert.Equal(AuthorRole.Assistant, response[0].Role); + } + + [Fact] + public async Task ShouldReturnValidAnthropicMetadataAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + var chatMessageContents = await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicResponse response = Deserialize( + await File.ReadAllTextAsync(ChatTestDataFilePath))!; + var textContent = chatMessageContents.SingleOrDefault(); + Assert.NotNull(textContent); + var metadata = textContent.Metadata as AnthropicMetadata; + Assert.NotNull(metadata); + Assert.Equal(response.FinishReason, metadata.FinishReason); + Assert.Equal(response.Id, metadata.MessageId); + Assert.Equal(response.StopSequence, metadata.StopSequence); + Assert.Equal(response.Usage.InputTokens, metadata.InputTokenCount); + Assert.Equal(response.Usage.OutputTokens, metadata.OutputTokenCount); + } + + [Fact] + public async Task ShouldReturnValidDictionaryMetadataAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + var chatMessageContents = await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicResponse response = Deserialize( + await File.ReadAllTextAsync(ChatTestDataFilePath))!; + var textContent = chatMessageContents.SingleOrDefault(); + Assert.NotNull(textContent); + var metadata = textContent.Metadata; + Assert.NotNull(metadata); + Assert.Equal(response.FinishReason, metadata[nameof(AnthropicMetadata.FinishReason)]); + Assert.Equal(response.Id, metadata[nameof(AnthropicMetadata.MessageId)]); + Assert.Equal(response.StopSequence, metadata[nameof(AnthropicMetadata.StopSequence)]); + Assert.Equal(response.Usage.InputTokens, metadata[nameof(AnthropicMetadata.InputTokenCount)]); + Assert.Equal(response.Usage.OutputTokens, metadata[nameof(AnthropicMetadata.OutputTokenCount)]); + } + + [Fact] + public async Task ShouldReturnResponseWithModelIdAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + var chatMessageContents = await client.GenerateChatMessageAsync(chatHistory); + + // Assert + var response = Deserialize( + await File.ReadAllTextAsync(ChatTestDataFilePath))!; + var chatMessageContent = chatMessageContents.SingleOrDefault(); + Assert.NotNull(chatMessageContent); + Assert.Equal(response.ModelId, chatMessageContent.ModelId); + } + + [Fact] + public async Task ShouldUsePromptExecutionSettingsAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + var executionSettings = new AnthropicPromptExecutionSettings() + { + MaxTokens = 102, + Temperature = 0.45, + TopP = 0.6f + }; + + // Act + await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings); + + // Assert + var request = Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Equal(executionSettings.MaxTokens, request.MaxTokens); + Assert.Equal(executionSettings.Temperature, request.Temperature); + Assert.Equal(executionSettings.TopP, request.TopP); + } + + [Fact] + public async Task ShouldThrowInvalidOperationExceptionIfChatHistoryContainsOnlySystemMessageAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = new ChatHistory("System message"); + + // Act & Assert + await Assert.ThrowsAsync( + () => client.GenerateChatMessageAsync(chatHistory)); + } + + [Fact] + public async Task ShouldThrowInvalidOperationExceptionIfChatHistoryContainsOnlyManySystemMessagesAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = new ChatHistory("System message"); + chatHistory.AddSystemMessage("System message 2"); + chatHistory.AddSystemMessage("System message 3"); + + // Act & Assert + await Assert.ThrowsAsync( + () => client.GenerateChatMessageAsync(chatHistory)); + } + + [Fact] + public async Task ShouldPassSystemMessageToRequestAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + string[] messages = ["System message", "System message 2"]; + var chatHistory = new ChatHistory(messages[0]); + chatHistory.AddSystemMessage(messages[1]); + chatHistory.AddUserMessage("Hello"); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicRequest? request = Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.NotNull(request.SystemPrompt); + Assert.All(messages, msg => Assert.Contains(msg, request.SystemPrompt, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task ShouldThrowArgumentExceptionIfChatHistoryIsEmptyAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = new ChatHistory(); + + // Act & Assert + await Assert.ThrowsAsync( + () => client.GenerateChatMessageAsync(chatHistory)); + } + + [Theory] + [InlineData(0)] + [InlineData(-15)] + public async Task ShouldThrowArgumentExceptionIfExecutionSettingMaxTokensIsLessThanOneAsync(int? maxTokens) + { + // Arrange + var client = this.CreateChatCompletionClient(); + AnthropicPromptExecutionSettings executionSettings = new() + { + MaxTokens = maxTokens + }; + + // Act & Assert + await Assert.ThrowsAsync( + () => client.GenerateChatMessageAsync(CreateSampleChatHistory(), executionSettings: executionSettings)); + } + + [Fact] + public async Task ItCreatesPostRequestAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.Equal(HttpMethod.Post, this._messageHandlerStub.Method); + } + + [Fact] + public async Task ItCreatesPostRequestWithValidUserAgentAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestHeaders); + Assert.Equal(HttpHeaderConstant.Values.UserAgent, this._messageHandlerStub.RequestHeaders.UserAgent.ToString()); + } + + [Fact] + public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + var expectedVersion = HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient)); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestHeaders); + var header = this._messageHandlerStub.RequestHeaders.GetValues(HttpHeaderConstant.Names.SemanticKernelVersion).SingleOrDefault(); + Assert.NotNull(header); + Assert.Equal(expectedVersion, header); + } + + [Fact] + public async Task ItCreatesPostRequestWithValidAnthropicVersionAsync() + { + // Arrange + var options = new AnthropicClientOptions(); + var client = this.CreateChatCompletionClient(options: options); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestHeaders); + Assert.Equal(options.Version, this._messageHandlerStub.RequestHeaders.GetValues("anthropic-version").SingleOrDefault()); + } + + [Fact] + public async Task ItCreatesPostRequestWithValidApiKeyAsync() + { + // Arrange + string apiKey = "fake-claude-key"; + var client = this.CreateChatCompletionClient(apiKey: apiKey); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestHeaders); + Assert.Equal(apiKey, this._messageHandlerStub.RequestHeaders.GetValues("x-api-key").SingleOrDefault()); + } + + [Fact] + public async Task ItCreatesPostRequestWithJsonContentTypeAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.NotNull(this._messageHandlerStub.ContentHeaders); + Assert.NotNull(this._messageHandlerStub.ContentHeaders.ContentType); + Assert.Contains("application/json", this._messageHandlerStub.ContentHeaders.ContentType.ToString()); + } + + [Fact] + public async Task ItCreatesPostRequestWithCustomUriAndCustomHeadersAsync() + { + // Arrange + Uri uri = new("https://fake-uri.com"); + (string headerName, string headerValue) = ("custom-header", "custom-value"); + ValueTask RequestHandler(HttpRequestMessage arg) + { + arg.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } + var client = new AnthropicClient( + httpClient: this._httpClient, + modelId: "fake-model", + options: null, + endpoint: uri, + requestHandler: RequestHandler); + + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.Equal(uri, this._messageHandlerStub.RequestUri); + Assert.NotNull(this._messageHandlerStub.RequestHeaders); + Assert.Equal(headerValue, this._messageHandlerStub.RequestHeaders.GetValues(headerName).SingleOrDefault()); + } + + private static ChatHistory CreateSampleChatHistory() + { + var chatHistory = new ChatHistory("You are a chatbot"); + chatHistory.AddUserMessage("Hello"); + chatHistory.AddAssistantMessage("Hi"); + chatHistory.AddUserMessage("How are you?"); + return chatHistory; + } + + private AnthropicClient CreateChatCompletionClient( + string modelId = "fake-model", + string? apiKey = null, + AnthropicClientOptions? options = null, + HttpClient? httpClient = null) + { + return new AnthropicClient( + httpClient: httpClient ?? this._httpClient, + modelId: modelId, + options: options, + apiKey: apiKey ?? "fake-key"); + } + + private static T? Deserialize(string json) + { + return JsonSerializer.Deserialize(json, options: AnthropicClient.SerializerOptions); + } + + private static T? Deserialize(ReadOnlySpan json) + { + return JsonSerializer.Deserialize(json, options: AnthropicClient.SerializerOptions); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs index fbc05591c9c1..5bd0d70a03b5 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs @@ -15,7 +15,7 @@ namespace SemanticKernel.Connectors.Anthropic.UnitTests.Core; public sealed class AnthropicRequestTests { [Fact] - public void FromChatHistoryItReturnsClaudeRequestWithConfiguration() + public void FromChatHistoryItReturnsWithConfiguration() { // Arrange ChatHistory chatHistory = []; @@ -42,7 +42,7 @@ public void FromChatHistoryItReturnsClaudeRequestWithConfiguration() [Theory] [InlineData(false)] [InlineData(true)] - public void FromChatHistoryItReturnsClaudeRequestWithValidStreamingMode(bool streamMode) + public void FromChatHistoryItReturnsWithValidStreamingMode(bool streamMode) { // Arrange ChatHistory chatHistory = []; @@ -65,7 +65,7 @@ public void FromChatHistoryItReturnsClaudeRequestWithValidStreamingMode(bool str } [Fact] - public void FromChatHistoryItReturnsClaudeRequestWithChatHistory() + public void FromChatHistoryItReturnsWithChatHistory() { // Arrange ChatHistory chatHistory = []; @@ -94,7 +94,7 @@ public void FromChatHistoryItReturnsClaudeRequestWithChatHistory() } [Fact] - public void FromChatHistoryTextAsTextContentItReturnsClaudeRequestWithChatHistory() + public void FromChatHistoryTextAsTextContentItReturnsWithChatHistory() { // Arrange ChatHistory chatHistory = []; @@ -119,7 +119,7 @@ public void FromChatHistoryTextAsTextContentItReturnsClaudeRequestWithChatHistor } [Fact] - public void FromChatHistoryImageAsImageContentItReturnsClaudeRequestWithChatHistory() + public void FromChatHistoryImageAsImageContentItReturnsWithChatHistory() { // Arrange ReadOnlyMemory imageAsBytes = new byte[] { 0x00, 0x01, 0x02, 0x03 }; @@ -174,121 +174,36 @@ public void FromChatHistoryUnsupportedContentItThrowsNotSupportedException() } [Fact] - public void AddFunctionItAddsFunctionToClaudeRequest() + public void FromChatHistoryItReturnsWithSystemMessages() { // Arrange - var request = new AnthropicRequest(); - var function = new AnthropicFunction("function-name", "function-description", "desc", null, null); - - // Act - request.AddFunction(function); - - // Assert - Assert.NotNull(request.Tools); - Assert.Collection(request.Tools, - func => Assert.Equivalent(function.ToFunctionDeclaration(), func, strict: true)); - } - - [Fact] - public void AddMultipleFunctionsItAddsFunctionsToClaudeRequest() - { - // Arrange - var request = new AnthropicRequest(); - var functions = new[] + string[] systemMessages = ["system-message1", "system-message2", "system-message3", "system-message4"]; + ChatHistory chatHistory = new(systemMessages[0]); + chatHistory.AddSystemMessage(systemMessages[1]); + chatHistory.Add(new ChatMessageContent(AuthorRole.System, + items: [new TextContent(systemMessages[2]), new TextContent(systemMessages[3])])); + chatHistory.AddUserMessage("user-message"); + var executionSettings = new AnthropicPromptExecutionSettings { - new AnthropicFunction("function-name", "function-description", "desc", null, null), - new AnthropicFunction("function-name2", "function-description2", "desc2", null, null) + ModelId = "claude", + MaxTokens = 128, }; - // Act - request.AddFunction(functions[0]); - request.AddFunction(functions[1]); - - // Assert - Assert.NotNull(request.Tools); - Assert.Collection(request.Tools, - func => Assert.Equivalent(functions[0].ToFunctionDeclaration(), func, strict: true), - func => Assert.Equivalent(functions[1].ToFunctionDeclaration(), func, strict: true)); - } - - [Fact] - public void FromChatHistoryCalledToolNotNullAddsFunctionResponse() - { - // Arrange - ChatHistory chatHistory = []; - var kvp = KeyValuePair.Create("sampleKey", "sampleValue"); - var expectedArgs = new JsonObject { [kvp.Key] = kvp.Value }; - var kernelFunction = KernelFunctionFactory.CreateFromMethod(() => ""); - var functionResult = new FunctionResult(kernelFunction, expectedArgs); - var toolCall = new AnthropicFunctionToolCall(new AnthropicToolCallContent { ToolId = "any uid", FunctionName = "function-name" }); - AnthropicFunctionToolResult toolCallResult = new(toolCall, functionResult, toolCall.ToolUseId); - chatHistory.Add(new AnthropicChatMessageContent(AuthorRole.Assistant, string.Empty, "modelId", toolCallResult)); - var executionSettings = new AnthropicPromptExecutionSettings { ModelId = "model-id", MaxTokens = 128 }; - // Act var request = AnthropicRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings); // Assert - Assert.Single(request.Messages, - c => c.Role == AuthorRole.Assistant); - Assert.Single(request.Messages, - c => c.Contents[0] is AnthropicToolResultContent); - Assert.Single(request.Messages, - c => c.Contents[0] is AnthropicToolResultContent toolResult - && string.Equals(toolResult.ToolId, toolCallResult.ToolUseId, StringComparison.Ordinal) - && toolResult.Content is AnthropicTextContent textContent - && string.Equals(functionResult.ToString(), textContent.Text, StringComparison.Ordinal)); - } - - [Fact] - public void FromChatHistoryToolCallsNotNullAddsFunctionCalls() - { - // Arrange - ChatHistory chatHistory = []; - var kvp = KeyValuePair.Create("sampleKey", "sampleValue"); - var expectedArgs = new JsonObject { [kvp.Key] = kvp.Value }; - var toolCallPart = new AnthropicToolCallContent - { ToolId = "any uid1", FunctionName = "function-name", Arguments = expectedArgs }; - var toolCallPart2 = new AnthropicToolCallContent - { ToolId = "any uid2", FunctionName = "function2-name", Arguments = expectedArgs }; - chatHistory.Add(new AnthropicChatMessageContent(AuthorRole.Assistant, "tool-message", "model-id", functionsToolCalls: [toolCallPart])); - chatHistory.Add(new AnthropicChatMessageContent(AuthorRole.Assistant, "tool-message2", "model-id2", functionsToolCalls: [toolCallPart2])); - var executionSettings = new AnthropicPromptExecutionSettings { ModelId = "model-id", MaxTokens = 128 }; - - // Act - var request = AnthropicRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings); - // Assert - Assert.Collection(request.Messages, - c => Assert.Equal(chatHistory[0].Role, c.Role), - c => Assert.Equal(chatHistory[1].Role, c.Role)); - Assert.Collection(request.Messages, - c => Assert.IsType(c.Contents[0]), - c => Assert.IsType(c.Contents[0])); - Assert.Collection(request.Messages, - c => - { - Assert.Equal(((AnthropicToolCallContent)c.Contents[0]).FunctionName, toolCallPart.FunctionName); - Assert.Equal(((AnthropicToolCallContent)c.Contents[0]).ToolId, toolCallPart.ToolId); - }, - c => - { - Assert.Equal(((AnthropicToolCallContent)c.Contents[0]).FunctionName, toolCallPart2.FunctionName); - Assert.Equal(((AnthropicToolCallContent)c.Contents[0]).ToolId, toolCallPart2.ToolId); - }); - Assert.Collection(request.Messages, - c => Assert.Equal(expectedArgs.ToJsonString(), - ((AnthropicToolCallContent)c.Contents[0]).Arguments!.ToJsonString()), - c => Assert.Equal(expectedArgs.ToJsonString(), - ((AnthropicToolCallContent)c.Contents[0]).Arguments!.ToJsonString())); + Assert.NotNull(request.SystemPrompt); + Assert.All(systemMessages, msg => Assert.Contains(msg, request.SystemPrompt, StringComparison.OrdinalIgnoreCase)); } [Fact] - public void AddChatMessageToRequestItAddsChatMessageToGeminiRequest() + public void AddChatMessageToRequestItAddsChatMessage() { // Arrange ChatHistory chat = []; var request = AnthropicRequest.FromChatHistoryAndExecutionSettings(chat, new AnthropicPromptExecutionSettings { ModelId = "model-id", MaxTokens = 128 }); - var message = new AnthropicChatMessageContent(AuthorRole.User, "user-message", "model-id"); + var message = new AnthropicChatMessageContent(AuthorRole.User, [new TextContent("user-message")], "model-id"); // Act request.AddChatMessage(message); @@ -300,9 +215,9 @@ public void AddChatMessageToRequestItAddsChatMessageToGeminiRequest() c => Equals(message.Role, c.Role)); } - private sealed class DummyContent : KernelContent - { - public DummyContent(object? innerContent, string? modelId = null, IReadOnlyDictionary? metadata = null) - : base(innerContent, modelId, metadata) { } - } + private sealed class DummyContent( + object? innerContent, + string? modelId = null, + IReadOnlyDictionary? metadata = null) + : KernelContent(innerContent, modelId, metadata); } diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionTests.cs deleted file mode 100644 index 863b058a8c94..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionTests.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.Anthropic; -using Xunit; - -namespace SemanticKernel.Connectors.Anthropic.UnitTests.Models; - -public sealed class AnthropicFunctionTests -{ - [Theory] - [InlineData(null, null, "", "")] - [InlineData("name", "description", "name", "description")] - public void ItInitializesClaudeFunctionParameterCorrectly(string? name, string? description, string expectedName, string expectedDescription) - { - // Arrange & Act - var schema = KernelJsonSchema.Parse("""{"type": "object" }"""); - var functionParameter = new ClaudeFunctionParameter(name, description, true, typeof(string), schema); - - // Assert - Assert.Equal(expectedName, functionParameter.Name); - Assert.Equal(expectedDescription, functionParameter.Description); - Assert.True(functionParameter.IsRequired); - Assert.Equal(typeof(string), functionParameter.ParameterType); - Assert.Same(schema, functionParameter.Schema); - } - - [Theory] - [InlineData(null, "")] - [InlineData("description", "description")] - public void ItInitializesClaudeFunctionReturnParameterCorrectly(string? description, string expectedDescription) - { - // Arrange & Act - var schema = KernelJsonSchema.Parse("""{"type": "object" }"""); - var functionParameter = new ClaudeFunctionReturnParameter(description, typeof(string), schema); - - // Assert - Assert.Equal(expectedDescription, functionParameter.Description); - Assert.Equal(typeof(string), functionParameter.ParameterType); - Assert.Same(schema, functionParameter.Schema); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithNoPluginName() - { - // Arrange - AnthropicFunction sut = KernelFunctionFactory.CreateFromMethod( - () => { }, "myfunc", "This is a description of the function.").Metadata.ToClaudeFunction(); - - // Act - var result = sut.ToFunctionDeclaration(); - - // Assert - Assert.Equal(sut.FunctionName, result.Name); - Assert.Equal(sut.Description, result.Description); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithNullParameters() - { - // Arrange - AnthropicFunction sut = new("plugin", "function", "description", null, null); - - // Act - var result = sut.ToFunctionDeclaration(); - - // Assert - Assert.Null(result.Parameters); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithPluginName() - { - // Arrange - AnthropicFunction sut = KernelPluginFactory.CreateFromFunctions("myplugin", new[] - { - KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.") - }).GetFunctionsMetadata()[0].ToClaudeFunction(); - - // Act - var result = sut.ToFunctionDeclaration(); - - // Assert - Assert.Equal($"myplugin{AnthropicFunction.NameSeparator}myfunc", result.Name); - Assert.Equal(sut.Description, result.Description); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() - { - string expectedParameterSchema = """ - { "type": "object", - "required": ["param1", "param2"], - "properties": { - "param1": { "type": "string", "description": "String param 1" }, - "param2": { "type": "integer", "description": "Int param 2" } } } - """; - - KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] - { - KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] - ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", - "TestFunction", - "My test function") - }); - - AnthropicFunction sut = plugin.GetFunctionsMetadata()[0].ToClaudeFunction(); - - var functionDefinition = sut.ToFunctionDeclaration(); - - Assert.NotNull(functionDefinition); - Assert.Equal($"Tests{AnthropicFunction.NameSeparator}TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), - JsonSerializer.Serialize(functionDefinition.Parameters)); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() - { - string expectedParameterSchema = """ - { "type": "object", - "required": ["param1", "param2"], - "properties": { - "param1": { "type": "string", "description": "String param 1" }, - "param2": { "type": "integer", "description": "Int param 2" } } } - """; - - KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] - { - KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] - ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, - "TestFunction", - "My test function") - }); - - AnthropicFunction sut = plugin.GetFunctionsMetadata()[0].ToClaudeFunction(); - - var functionDefinition = sut.ToFunctionDeclaration(); - - Assert.NotNull(functionDefinition); - Assert.Equal($"Tests{AnthropicFunction.NameSeparator}TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), - JsonSerializer.Serialize(functionDefinition.Parameters)); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() - { - // Arrange - AnthropicFunction f = KernelFunctionFactory.CreateFromMethod( - () => { }, - parameters: new[] { new KernelParameterMetadata("param1") }).Metadata.ToClaudeFunction(); - - // Act - var result = f.ToFunctionDeclaration(); - - // Assert - Assert.Equal( - """{"type":"object","required":[],"properties":{"param1":{"type":"string"}}}""", - JsonSerializer.Serialize(result.Parameters)); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescriptions() - { - // Arrange - AnthropicFunction f = KernelFunctionFactory.CreateFromMethod( - () => { }, - parameters: new[] { new KernelParameterMetadata("param1") { Description = "something neat" } }).Metadata.ToClaudeFunction(); - - // Act - var result = f.ToFunctionDeclaration(); - - // Assert - Assert.Equal( - """{"type":"object","required":[],"properties":{"param1":{"type":"string","description":"something neat"}}}""", - JsonSerializer.Serialize(result.Parameters)); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionToolCallTests.cs deleted file mode 100644 index e178393dac7b..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Models/AnthropicFunctionToolCallTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Globalization; -using System.Text.Json.Nodes; -using Microsoft.SemanticKernel.Connectors.Anthropic; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; -using Xunit; - -namespace SemanticKernel.Connectors.Anthropic.UnitTests.Models; - -/// -/// Unit tests for class. -/// -public sealed class AnthropicFunctionToolCallTests -{ - [Theory] - [InlineData("MyFunction")] - [InlineData("MyPlugin_MyFunction")] - public void FullyQualifiedNameReturnsValidName(string toolCallName) - { - // Arrange - var toolCallPart = new AnthropicToolCallContent { FunctionName = toolCallName }; - var functionToolCall = new AnthropicFunctionToolCall(toolCallPart); - - // Act & Assert - Assert.Equal(toolCallName, functionToolCall.FullyQualifiedName); - } - - [Fact] - public void ArgumentsReturnsCorrectValue() - { - // Arrange - var toolCallPart = new AnthropicToolCallContent - { - FunctionName = "MyPlugin_MyFunction", - Arguments = new JsonObject - { - { "location", "San Diego" }, - { "max_price", 300 } - } - }; - var functionToolCall = new AnthropicFunctionToolCall(toolCallPart); - - // Act & Assert - Assert.NotNull(functionToolCall.Arguments); - Assert.Equal(2, functionToolCall.Arguments.Count); - Assert.Equal("San Diego", functionToolCall.Arguments["location"]!.ToString()); - Assert.Equal(300, - Convert.ToInt32(functionToolCall.Arguments["max_price"]!.ToString(), new NumberFormatInfo())); - } - - [Fact] - public void ToStringReturnsCorrectValue() - { - // Arrange - var toolCallPart = new AnthropicToolCallContent - { - FunctionName = "MyPlugin_MyFunction", - Arguments = new JsonObject - { - { "location", "San Diego" }, - { "max_price", 300 } - } - }; - var functionToolCall = new AnthropicFunctionToolCall(toolCallPart); - - // Act & Assert - Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", functionToolCall.ToString()); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/TestData/chat_one_response.json b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/TestData/chat_one_response.json new file mode 100644 index 000000000000..ac0e04ce73a8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/TestData/chat_one_response.json @@ -0,0 +1,18 @@ +{ + "content": [ + { + "text": "Hi! My name is Claude.", + "type": "text" + } + ], + "id": "msg_013Zva2CMHLNnXjNJJKqJ2EF", + "model": "claude-3-5-sonnet-20240620", + "role": "assistant", + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "input_tokens": 10, + "output_tokens": 25 + } +} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Utils/AnthropicKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Utils/AnthropicKernelFunctionMetadataExtensions.cs deleted file mode 100644 index 04607cc5b643..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Utils/AnthropicKernelFunctionMetadataExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.Anthropic; - -namespace SemanticKernel.Connectors.Anthropic.UnitTests; - -/// -/// Extensions for specific to the Claude connector. -/// -public static class AnthropicKernelFunctionMetadataExtensions -{ - /// - /// Convert a to an . - /// - /// The object to convert. - /// An object. - public static AnthropicFunction ToClaudeFunction(this KernelFunctionMetadata metadata) - { - IReadOnlyList metadataParams = metadata.Parameters; - - var openAIParams = new ClaudeFunctionParameter[metadataParams.Count]; - for (int i = 0; i < openAIParams.Length; i++) - { - var param = metadataParams[i]; - - openAIParams[i] = new ClaudeFunctionParameter( - param.Name, - GetDescription(param), - param.IsRequired, - param.ParameterType, - param.Schema); - } - - return new AnthropicFunction( - metadata.PluginName, - metadata.Name, - metadata.Description, - openAIParams, - new ClaudeFunctionReturnParameter( - metadata.ReturnParameter.Description, - metadata.ReturnParameter.ParameterType, - metadata.ReturnParameter.Schema)); - - static string GetDescription(KernelParameterMetadata param) - { - string? stringValue = InternalTypeConverter.ConvertToString(param.DefaultValue); - return !string.IsNullOrEmpty(stringValue) ? $"{param.Description} (default value: {stringValue})" : param.Description; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs index 1bbcecf1fcae..19ad4e1b4158 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs @@ -27,7 +27,15 @@ public enum ServiceVersion internal string Version { get; } - /// Initializes new instance of OpenAIClientOptions. + /// + /// Initializes new instance of + /// + /// + /// This parameter is optional. + /// Default value is .
+ /// The version is ignored when used with other vendor than Anthropic. + /// + /// Provided version is not supported. public AnthropicClientOptions(ServiceVersion version = LatestVersion) { this.Version = version switch diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs index 1b5b8713d5e5..482586a23794 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs @@ -21,7 +21,6 @@ public sealed class AnthropicPromptExecutionSettings : PromptExecutionSettings private int? _topK; private int? _maxTokens; private IList? _stopSequences; - private AnthropicToolCallBehavior? _toolCallBehavior; /// /// Default max tokens for a text generation. @@ -103,43 +102,6 @@ public IList? StopSequences } } - /// - /// Gets or sets the behavior for how tool calls are handled. - /// - /// - /// - /// To disable all tool calling, set the property to null (the default). - /// - /// To allow the model to request one of any number of functions, set the property to an - /// instance returned from , called with - /// a list of the functions available. - /// - /// - /// To allow the model to request one of any of the functions in the supplied , - /// set the property to if the client should simply - /// send the information about the functions and not handle the response in any special manner, or - /// if the client should attempt to automatically - /// invoke the function and send the result back to the service. - /// - /// - /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service - /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to - /// resolve that function from the functions available in the , and if found, rather - /// than returning the response back to the caller, it will handle the request automatically, invoking - /// the function, and sending back the result. The intermediate messages will be retained in the - /// if an instance was provided. - /// - public AnthropicToolCallBehavior? ToolCallBehavior - { - get => this._toolCallBehavior; - - set - { - this.ThrowIfFrozen(); - this._toolCallBehavior = value; - } - } - /// public override void Freeze() { @@ -168,7 +130,6 @@ public override PromptExecutionSettings Clone() TopK = this.TopK, MaxTokens = this.MaxTokens, StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, - ToolCallBehavior = this.ToolCallBehavior?.Clone(), }; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicToolCallBehavior.cs deleted file mode 100644 index 241a90675c12..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicToolCallBehavior.cs +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic; - -/// Represents a behavior for Claude tool calls. -public abstract class AnthropicToolCallBehavior -{ - // NOTE: Right now, the only tools that are available are for function calling. In the future, - // this class can be extended to support additional kinds of tools, including composite ones: - // the ClaudePromptExecutionSettings has a single ToolCallBehavior property, but we could - // expose a `public static ToolCallBehavior Composite(params ToolCallBehavior[] behaviors)` - // or the like to allow multiple distinct tools to be provided, should that be appropriate. - // We can also consider additional forms of tools, such as ones that dynamically examine - // the Kernel, KernelArguments, etc., and dynamically contribute tools to the ChatCompletionsOptions. - - /// - /// The default maximum number of tool-call auto-invokes that can be made in a single request. - /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled (e.g. will behave like )). - /// This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. It is currently hardcoded, but in the future it could - /// be made configurable by the developer. Other configuration is also possible in the future, - /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure - /// to find the requested function, failure to invoke the function, etc.), with behaviors for - /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call - /// support, where the model can request multiple tools in a single response, it is significantly - /// less likely that this limit is reached, as most of the time only a single request is needed. - /// - private const int DefaultMaximumAutoInvokeAttempts = 5; - - /// - /// Gets an instance that will provide all of the 's plugins' function information. - /// Function call requests from the model will be propagated back to the caller. - /// - /// - /// If no is available, no function information will be provided to the model. - /// - public static AnthropicToolCallBehavior EnableKernelFunctions => new KernelFunctions(autoInvoke: false); - - /// - /// Gets an instance that will both provide all of the 's plugins' function information - /// to the model and attempt to automatically handle any function call requests. - /// - /// - /// When successful, tool call requests from the model become an implementation detail, with the service - /// handling invoking any requested functions and supplying the results back to the model. - /// If no is available, no function information will be provided to the model. - /// - public static AnthropicToolCallBehavior AutoInvokeKernelFunctions => new KernelFunctions(autoInvoke: true); - - /// Gets an instance that will provide the specified list of functions to the model. - /// The functions that should be made available to the model. - /// true to attempt to automatically handle function call requests; otherwise, false. - /// - /// The that may be set into - /// to indicate that the specified functions should be made available to the model. - /// - public static AnthropicToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) - { - Verify.NotNull(functions); - return new EnabledFunctions(functions, autoInvoke); - } - - /// Initializes the instance; prevents external instantiation. - private AnthropicToolCallBehavior(bool autoInvoke) - { - this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; - } - - /// Gets how many requests are part of a single interaction should include this tool in the request. - /// - /// This should be greater than or equal to . It defaults to . - /// Once this limit is reached, the tools will no longer be included in subsequent retries as part of the operation, e.g. - /// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result - /// will not include the tools for further use. - /// - public int MaximumUseAttempts { get; } = int.MaxValue; - - /// Gets how many tool call request/response roundtrips are supported with auto-invocation. - /// - /// To disable auto invocation, this can be set to 0. - /// - public int MaximumAutoInvokeAttempts { get; } - - /// - /// Gets whether validation against a specified list is required before allowing the model to request a function from the kernel. - /// - /// true if it's ok to invoke any kernel function requested by the model if it's found; - /// false if a request needs to be validated against an allow list. - internal virtual bool AllowAnyRequestedKernelFunction => false; - - /// Configures the with any tools this provides. - /// The used for the operation. - /// This can be queried to determine what tools to provide into the . - /// The destination to configure. - internal abstract void ConfigureClaudeRequest(Kernel? kernel, AnthropicRequest request); - - internal AnthropicToolCallBehavior Clone() - { - return (AnthropicToolCallBehavior)this.MemberwiseClone(); - } - - /// - /// Represents a that will provide to the model all available functions from a - /// provided by the client. - /// - internal sealed class KernelFunctions : AnthropicToolCallBehavior - { - internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } - - public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - - internal override void ConfigureClaudeRequest(Kernel? kernel, AnthropicRequest request) - { - // If no kernel is provided, we don't have any tools to provide. - if (kernel is null) - { - return; - } - - // Provide all functions from the kernel. - foreach (var functionMetadata in kernel.Plugins.GetFunctionsMetadata()) - { - request.AddFunction(FunctionMetadataAsClaudeFunction(functionMetadata)); - } - } - - internal override bool AllowAnyRequestedKernelFunction => true; - - /// - /// Convert a to an . - /// - /// The object to convert. - /// An object. - private static AnthropicFunction FunctionMetadataAsClaudeFunction(KernelFunctionMetadata metadata) - { - IReadOnlyList metadataParams = metadata.Parameters; - - var openAIParams = new ClaudeFunctionParameter[metadataParams.Count]; - for (int i = 0; i < openAIParams.Length; i++) - { - var param = metadataParams[i]; - - openAIParams[i] = new ClaudeFunctionParameter( - param.Name, - GetDescription(param), - param.IsRequired, - param.ParameterType, - param.Schema); - } - - return new AnthropicFunction( - metadata.PluginName, - metadata.Name, - metadata.Description, - openAIParams, - new ClaudeFunctionReturnParameter( - metadata.ReturnParameter.Description, - metadata.ReturnParameter.ParameterType, - metadata.ReturnParameter.Schema)); - - static string GetDescription(KernelParameterMetadata param) - { - string? stringValue = InternalTypeConverter.ConvertToString(param.DefaultValue); - return !string.IsNullOrEmpty(stringValue) ? $"{param.Description} (default value: {stringValue})" : param.Description; - } - } - } - - /// - /// Represents a that provides a specified list of functions to the model. - /// - internal sealed class EnabledFunctions : AnthropicToolCallBehavior - { - private readonly AnthropicFunction[] _functions; - - public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) - { - this._functions = functions.ToArray(); - } - - public override string ToString() => - $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): " + - $"{string.Join(", ", this._functions.Select(f => f.FunctionName))}"; - - internal override void ConfigureClaudeRequest(Kernel? kernel, AnthropicRequest request) - { - if (this._functions.Length == 0) - { - return; - } - - bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; - - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) - { - throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); - } - - foreach (var func in this._functions) - { - // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. - if (autoInvoke) - { - if (!kernel!.Plugins.TryGetFunction(func.PluginName, func.FunctionName, out _)) - { - throw new KernelException( - $"The specified {nameof(EnabledFunctions)} function {func.FullyQualifiedName} is not available in the kernel."); - } - } - - // Add the function. - request.AddFunction(func); - } - } - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index acb0c1f3411e..1e77a05a6b18 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -5,7 +5,9 @@ using System.Diagnostics.Metrics; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -14,6 +16,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; @@ -24,12 +27,19 @@ internal sealed class AnthropicClient { private const string ModelProvider = "anthropic"; + internal static JsonSerializerOptions SerializerOptions { get; } + = new(JsonOptionsCache.Default) + { + Converters = { new PolymorphicJsonConverterFactory() }, + TypeInfoResolver = JsonTypeDiscriminatorHelper.TypeInfoResolver + }; + private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly string _modelId; private readonly string? _apiKey; private readonly Uri _endpoint; - private readonly Func? _customRequestHandler; + private readonly Func? _customRequestHandler; private readonly AnthropicClientOptions _options; private static readonly string s_namespace = typeof(AnthropicChatCompletionService).Namespace!; @@ -106,7 +116,7 @@ public AnthropicClient( HttpClient httpClient, string modelId, Uri endpoint, - Func? requestHandler, + Func? requestHandler, AnthropicClientOptions? options, ILogger? logger = null) { @@ -141,13 +151,12 @@ public async Task> GenerateChatMessageAsync( using var activity = ModelDiagnostics.StartCompletionActivity( this._endpoint, this._modelId, ModelProvider, chatHistory, state.ExecutionSettings); - List chatResponses; + List chatResponses; AnthropicResponse anthropicResponse; try { anthropicResponse = await this.SendRequestAndReturnValidResponseAsync( - this._endpoint, state.AnthropicRequest, cancellationToken) - .ConfigureAwait(false); + this._endpoint, state.AnthropicRequest, cancellationToken).ConfigureAwait(false); chatResponses = this.GetChatResponseFrom(anthropicResponse); } catch (Exception ex) when (activity is not null) @@ -164,16 +173,16 @@ public async Task> GenerateChatMessageAsync( return chatResponses; } - private List GetChatResponseFrom(AnthropicResponse response) + private List GetChatResponseFrom(AnthropicResponse response) { var chatMessageContents = this.GetChatMessageContentsFromResponse(response); this.LogUsage(chatMessageContents); return chatMessageContents; } - private void LogUsage(List chatMessageContents) + private void LogUsage(List chatMessageContents) { - if (chatMessageContents[0].Metadata is not AnthropicMetadata { TotalTokenCount: > 0 } metadata) + if (chatMessageContents[0].Metadata is not { TotalTokenCount: > 0 } metadata) { this.Log(LogLevel.Debug, "Token usage information unavailable."); return; @@ -190,20 +199,21 @@ private void LogUsage(List chatMessageContents) s_totalTokensCounter.Add(metadata.TotalTokenCount); } - private List GetChatMessageContentsFromResponse(AnthropicResponse response) - => response.Contents!.Select(content => this.GetChatMessageContentFromAnthropicContent(response, content)).ToList(); + private List GetChatMessageContentsFromResponse(AnthropicResponse response) + => response.Contents.Select(content => this.GetChatMessageContentFromAnthropicContent(response, content)).ToList(); - private ChatMessageContent GetChatMessageContentFromAnthropicContent(AnthropicResponse response, AnthropicContent content) + private AnthropicChatMessageContent GetChatMessageContentFromAnthropicContent(AnthropicResponse response, AnthropicContent content) { if (content is not AnthropicTextContent textContent) { throw new NotSupportedException($"Content type {content.GetType()} is not supported yet."); } - return new ChatMessageContent( + return new AnthropicChatMessageContent( role: response.Role, - content: textContent.Text ?? string.Empty, + items: [new TextContent(textContent.Text ?? string.Empty)], modelId: response.ModelId ?? this._modelId, + innerContent: response, metadata: GetResponseMetadata(response)); } @@ -211,7 +221,7 @@ private static AnthropicMetadata GetResponseMetadata(AnthropicResponse response) => new() { MessageId = response.Id, - FinishReason = response.StopReason, + FinishReason = response.FinishReason, StopSequence = response.StopSequence, InputTokenCount = response.Usage?.InputTokens ?? 0, OutputTokenCount = response.Usage?.OutputTokens ?? 0 @@ -246,6 +256,7 @@ private ChatCompletionState ValidateInputAndCreateChatCompletionState( var anthropicExecutionSettings = AnthropicPromptExecutionSettings.FromExecutionSettings(executionSettings); ValidateMaxTokens(anthropicExecutionSettings.MaxTokens); + anthropicExecutionSettings.ModelId ??= this._modelId; this.Log(LogLevel.Trace, "ChatHistory: {ChatHistory}, Settings: {Settings}", JsonSerializer.Serialize(chatHistory), @@ -324,7 +335,7 @@ private static T DeserializeResponse(string body) { try { - return JsonSerializer.Deserialize(body) ?? throw new JsonException("Response is null"); + return JsonSerializer.Deserialize(body, options: SerializerOptions) ?? throw new JsonException("Response is null"); } catch (JsonException exc) { @@ -337,7 +348,7 @@ private static T DeserializeResponse(string body) private async Task CreateHttpRequestAsync(object requestData, Uri endpoint) { - var httpRequestMessage = HttpRequest.CreatePostRequest(endpoint, requestData); + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = CreateJsonContent(requestData) }; httpRequestMessage.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); httpRequestMessage.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); @@ -346,10 +357,31 @@ private async Task CreateHttpRequestAsync(object requestData { await this._customRequestHandler(httpRequestMessage).ConfigureAwait(false); } + else + { + httpRequestMessage.Headers.Add("anthropic-version", this._options.Version); + httpRequestMessage.Headers.Add("x-api-key", this._apiKey); + } return httpRequestMessage; } + private static HttpContent? CreateJsonContent(object? payload) + { + HttpContent? content = null; + if (payload is not null) + { + byte[] utf8Bytes = payload is string s + ? Encoding.UTF8.GetBytes(s) + : JsonSerializer.SerializeToUtf8Bytes(payload, SerializerOptions); + + content = new ByteArrayContent(utf8Bytes); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; + } + + return content; + } + private void Log(LogLevel logLevel, string? message, params object?[] args) { if (this._logger.IsEnabled(logLevel)) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 4dfc04e15ca7..eb4b5307bee6 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; @@ -24,10 +24,6 @@ internal sealed class AnthropicRequest [JsonPropertyName("messages")] public IList Messages { get; set; } = null!; - [JsonPropertyName("tools")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IList? Tools { get; set; } - [JsonPropertyName("model")] public string ModelId { get; set; } = null!; @@ -80,12 +76,6 @@ internal sealed class AnthropicRequest [JsonPropertyName("top_k")] public int? TopK { get; set; } - public void AddFunction(AnthropicFunction function) - { - this.Tools ??= new List(); - this.Tools.Add(function.ToFunctionDeclaration()); - } - public void AddChatMessage(ChatMessageContent message) { Verify.NotNull(this.Messages); @@ -145,36 +135,42 @@ private static AnthropicRequest CreateRequest(ChatHistory chatHistory, Anthropic private static List CreateClaudeMessages(ChatMessageContent content) { - var messages = content.Items.Select(GetClaudeMessageFromKernelContent).ToList(); - - if (messages.Count == 0) - { - messages.Add(new AnthropicTextContent(content.Content ?? string.Empty)); - } - - return messages; + return content.Items.Select(GetClaudeMessageFromKernelContent).ToList(); } private static AnthropicContent GetClaudeMessageFromKernelContent(KernelContent content) => content switch { - TextContent textContent => new AnthropicTextContent(textContent.Text ?? string.Empty), - ImageContent imageContent => new AnthropicImageContent( - type: "base64", - mediaType: imageContent.MimeType ?? throw new InvalidOperationException("Image content must have a MIME type."), - data: imageContent.Data.HasValue - ? Convert.ToBase64String(imageContent.Data.Value.ToArray()) - : throw new InvalidOperationException("Image content must have a data.") - ), + TextContent textContent => new AnthropicTextContent { Text = textContent.Text ?? string.Empty }, + ImageContent imageContent => CreateAnthropicImageContent(imageContent), _ => throw new NotSupportedException($"Content type '{content.GetType().Name}' is not supported.") }; + private static AnthropicImageContent CreateAnthropicImageContent(ImageContent imageContent) + { + var dataUri = DataUriParser.Parse(imageContent.DataUri); + if (dataUri.DataFormat?.Equals("base64", StringComparison.OrdinalIgnoreCase) != true) + { + throw new InvalidOperationException("Image content must be base64 encoded."); + } + + return new AnthropicImageContent + { + Source = new() + { + Type = dataUri.DataFormat, + MediaType = imageContent.MimeType ?? throw new InvalidOperationException("Image content must have a MIME type."), + Data = dataUri.Data ?? throw new InvalidOperationException("Image content must have a data.") + } + }; + } + internal sealed class Message { [JsonConverter(typeof(AuthorRoleConverter))] [JsonPropertyName("role")] - public AuthorRole Role { get; set; } + public AuthorRole Role { get; init; } [JsonPropertyName("content")] - public IList Contents { get; set; } = null!; + public IList Contents { get; init; } = null!; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs index 517717b81e3d..b5e57540a50b 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs @@ -30,7 +30,7 @@ internal sealed class AnthropicResponse public string ModelId { get; init; } = null!; [JsonPropertyName("stop_reason")] - public AnthropicFinishReason? StopReason { get; init; } + public AnthropicFinishReason? FinishReason { get; init; } [JsonPropertyName("stop_sequence")] public string? StopSequence { get; init; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs deleted file mode 100644 index f16cdb73274e..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -/// -/// A Tool is a piece of code that enables the system to interact with external systems to perform an action, -/// or set of actions, outside the knowledge and scope of the model. -/// Structured representation of a function declaration as defined by the OpenAPI 3.03 specification. -/// Included in this declaration are the function name and parameters. -/// This FunctionDeclaration is a representation of a block of code that can be used as a Tool by the model and executed by the client. -/// -internal sealed class AnthropicToolFunctionDeclaration -{ - /// - /// Required. Name of function. - /// - /// - /// Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 63. - /// - [JsonPropertyName("name")] - public string Name { get; set; } = null!; - - /// - /// Required. A brief description of the function. - /// - [JsonPropertyName("description")] - public string Description { get; set; } = null!; - - /// - /// Optional. Describes the parameters to this function. - /// Reflects the Open API 3.03 Parameter Object string Key: the name of the parameter. - /// Parameter names are case-sensitive. Schema Value: the Schema defining the type used for the parameter. - /// - [JsonPropertyName("parameters")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonNode? Parameters { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index 97b083dede66..dea8a7554054 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -7,11 +7,8 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// /// Represents the request/response content of Claude. /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(AnthropicTextContent), typeDiscriminator: "text")] -[JsonDerivedType(typeof(AnthropicTextContent), typeDiscriminator: "text_delta")] -[JsonDerivedType(typeof(AnthropicJsonDeltaContent), typeDiscriminator: "input_json_delta")] -[JsonDerivedType(typeof(AnthropicImageContent), typeDiscriminator: "image")] -[JsonDerivedType(typeof(AnthropicToolCallContent), typeDiscriminator: "tool_use")] -[JsonDerivedType(typeof(AnthropicToolResultContent), typeDiscriminator: "tool_result")] +[HackyJsonDerived(typeof(AnthropicTextContent), typeDiscriminator: "text")] +[HackyJsonDerived(typeof(AnthropicDeltaTextContent), typeDiscriminator: "text_delta")] +[HackyJsonDerived(typeof(AnthropicDeltaJsonContent), typeDiscriminator: "input_json_delta")] +[HackyJsonDerived(typeof(AnthropicImageContent), typeDiscriminator: "image")] internal abstract class AnthropicContent; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs similarity index 67% rename from dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs index a068d612bcd7..6be43608945c 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicJsonDeltaContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs @@ -4,14 +4,8 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; -internal sealed class AnthropicJsonDeltaContent +internal sealed class AnthropicDeltaJsonContent : AnthropicContent { - [JsonConstructor] - public AnthropicJsonDeltaContent(string partialJson) - { - this.PartialJson = partialJson; - } - /// /// Only used when type is "input_json_delta". The partial json content. /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs new file mode 100644 index 000000000000..70f275be92c2 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +internal sealed class AnthropicDeltaTextContent : AnthropicContent +{ + /// + /// Only used when type is "text". The text content. + /// + [JsonRequired] + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs index 8dd517267cdf..ea38ea5e0d76 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs @@ -6,12 +6,6 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; internal sealed class AnthropicImageContent : AnthropicContent { - [JsonConstructor] - public AnthropicImageContent(string type, string mediaType, string data) - { - this.Source = new SourceEntity(type, mediaType, data); - } - /// /// Only used when type is "image". The image content. /// @@ -20,14 +14,6 @@ public AnthropicImageContent(string type, string mediaType, string data) internal sealed class SourceEntity { - [JsonConstructor] - internal SourceEntity(string type, string mediaType, string data) - { - this.Type = type; - this.MediaType = mediaType; - this.Data = data; - } - /// /// Currently supported only base64. /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs index 80f3aea31caa..58256cb99e81 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs @@ -6,12 +6,6 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; internal sealed class AnthropicTextContent : AnthropicContent { - [JsonConstructor] - public AnthropicTextContent(string text) - { - this.Text = text; - } - /// /// Only used when type is "text". The text content. /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolCallContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolCallContent.cs deleted file mode 100644 index e738b3773221..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolCallContent.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class AnthropicToolCallContent : AnthropicContent -{ - [JsonPropertyName("id")] - [JsonRequired] - public string ToolId { get; set; } = null!; - - [JsonPropertyName("name")] - [JsonRequired] - public string FunctionName { get; set; } = null!; - - /// - /// Optional. The function parameters and values in JSON object format. - /// - [JsonPropertyName("input")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonNode? Arguments { get; set; } - - /// - public override string ToString() - { - return $"FunctionName={this.FunctionName}, Arguments={this.Arguments}"; - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolResultContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolResultContent.cs deleted file mode 100644 index dcf2c31f4965..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicToolResultContent.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class AnthropicToolResultContent : AnthropicContent -{ - [JsonPropertyName("tool_use_id")] - [JsonRequired] - public string ToolId { get; set; } = null!; - - [JsonPropertyName("content")] - [JsonRequired] - public AnthropicContent Content { get; set; } = null!; - - [JsonPropertyName("is_error")] - public bool IsError { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs new file mode 100644 index 000000000000..ff76a5bc14ae --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +// Temporary solution from https://github.com/dotnet/runtime/issues/72604 +// TODO: Remove this once we move to .NET 9 + +internal static class JsonTypeDiscriminatorHelper +{ + internal static IJsonTypeInfoResolver TypeInfoResolver { get; } = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + var propertyNamingPolicy = typeInfo.Options.PropertyNamingPolicy; + + // Temporary hack to ensure subclasses of abstract classes will always include the type field + if (typeInfo.Type.BaseType is { IsAbstract: true } && + typeInfo.Type.BaseType.GetCustomAttributes().Any()) + { + var discriminatorPropertyName = propertyNamingPolicy?.ConvertName("type") ?? "type"; + if (typeInfo.Properties.All(p => p.Name != discriminatorPropertyName)) + { + var discriminatorValue = typeInfo.Type.BaseType + .GetCustomAttributes() + .First(attr => attr.Subtype == typeInfo.Type).TypeDiscriminator; + var propInfo = typeInfo.CreateJsonPropertyInfo(typeof(string), discriminatorPropertyName); + propInfo.Get = _ => discriminatorValue; + typeInfo.Properties.Insert(0, propInfo); + } + } + }, + }, + }; +} + +/// +/// Same as but used for the hack below. Necessary because using the built-in +/// attribute will lead to NotSupportedExceptions. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] +internal sealed class HackyJsonDerivedAttribute(Type subtype, string typeDiscriminator) : Attribute +{ + public Type Subtype { get; set; } = subtype; + public string TypeDiscriminator { get; set; } = typeDiscriminator; +} + +internal sealed class PolymorphicJsonConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAbstract && typeToConvert.GetCustomAttributes().Any(); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter?)Activator.CreateInstance( + typeof(PolymorphicJsonConverter<>).MakeGenericType(typeToConvert), options); + } +} + +/// +/// A temporary hack to support deserializing JSON payloads that use polymorphism but don't specify type as the first field. +/// Modified from https://github.com/dotnet/runtime/issues/72604#issuecomment-1440708052. +/// +internal sealed class PolymorphicJsonConverter : JsonConverter +{ + private readonly string _discriminatorPropName; + private readonly Dictionary _discriminatorToSubtype = []; + + public PolymorphicJsonConverter(JsonSerializerOptions options) + { + this._discriminatorPropName = options.PropertyNamingPolicy?.ConvertName("type") ?? "type"; + foreach (var subtype in typeof(T).GetCustomAttributes()) + { + this._discriminatorToSubtype.Add(subtype.TypeDiscriminator, subtype.Subtype); + } + } + + public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert; + + public override T Read( + ref Utf8JsonReader reader, Type objectType, JsonSerializerOptions options) + { + var reader2 = reader; + using var doc = JsonDocument.ParseValue(ref reader2); + + var root = doc.RootElement; + var typeField = root.GetProperty(this._discriminatorPropName); + + if (typeField.GetString() is not { } typeName) + { + throw new JsonException( + $"Could not find string property {this._discriminatorPropName} " + + $"when trying to deserialize {typeof(T).Name}"); + } + + if (!this._discriminatorToSubtype.TryGetValue(typeName, out var type)) + { + throw new JsonException($"Unknown type: {typeName}"); + } + + return (T)JsonSerializer.Deserialize(ref reader, type, options)!; + } + + public override void Write( + Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + var type = value!.GetType(); + JsonSerializer.Serialize(writer, value, type, options); + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index f5258d9b630f..e0f361bc50dd 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -63,7 +63,7 @@ public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, string modelId, Uri endpoint, - Func? requestHandler, + Func? requestHandler, AnthropicClientOptions? options = null, string? serviceId = null, HttpClient? httpClient = null) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 9e92b2ea8857..1852f8d3f52e 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -60,7 +60,7 @@ public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, string modelId, Uri endpoint, - Func? requestHandler, + Func? requestHandler, AnthropicClientOptions? options = null, string? serviceId = null) { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs index f0a291226bef..6788fc8da7f4 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs @@ -3,93 +3,44 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic.Core; namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// -/// Claude specialized chat message content +/// Anthropic specialized chat message content /// public sealed class AnthropicChatMessageContent : ChatMessageContent { /// - /// Initializes a new instance of the class. + /// Creates a new instance of the class /// - /// The result of tool called by the kernel. - public AnthropicChatMessageContent(AnthropicFunctionToolResult calledToolResult) - : base( - role: AuthorRole.Assistant, - content: null, - modelId: null, - innerContent: null, - encoding: Encoding.UTF8, - metadata: null) - { - Verify.NotNull(calledToolResult); - - this.CalledToolResult = calledToolResult; - } + [JsonConstructor] + internal AnthropicChatMessageContent() { } /// /// Initializes a new instance of the class. /// /// Role of the author of the message - /// Content of the message + /// Instance of with content items /// The model ID used to generate the content - /// The result of tool called by the kernel. + /// Inner content object reference /// Additional metadata internal AnthropicChatMessageContent( AuthorRole role, - string? content, + ChatMessageContentItemCollection items, string modelId, - AnthropicFunctionToolResult? calledToolResult = null, + object? innerContent = null, AnthropicMetadata? metadata = null) : base( role: role, - content: content, + items: items, modelId: modelId, - innerContent: content, + innerContent: innerContent, encoding: Encoding.UTF8, - metadata: metadata) - { - this.CalledToolResult = calledToolResult; - } - - /// - /// Initializes a new instance of the class. - /// - /// Role of the author of the message - /// Content of the message - /// The model ID used to generate the content - /// Tool calls parts returned by model - /// Additional metadata - internal AnthropicChatMessageContent( - AuthorRole role, - string? content, - string modelId, - IEnumerable? functionsToolCalls, - AnthropicMetadata? metadata = null) - : base( - role: role, - content: content, - modelId: modelId, - innerContent: content, - encoding: Encoding.UTF8, - metadata: metadata) - { - this.ToolCalls = functionsToolCalls?.Select(tool => new AnthropicFunctionToolCall(tool)).ToList(); - } - - /// - /// A list of the tools returned by the model with arguments. - /// - public IReadOnlyList? ToolCalls { get; } - - /// - /// The result of tool called by the kernel. - /// - public AnthropicFunctionToolResult? CalledToolResult { get; } + metadata: metadata) { } /// /// The metadata associated with the content. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs deleted file mode 100644 index 60896e39bff9..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunction.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic; - -// NOTE: Since this space is evolving rapidly, in order to reduce the risk of needing to take breaking -// changes as Anthropic's APIs evolve, these types are not externally constructible. In the future, once -// things stabilize, and if need demonstrates, we could choose to expose those constructors. - -/// -/// Represents a function parameter that can be passed to an Anthropic function tool call. -/// -public sealed class ClaudeFunctionParameter -{ - internal ClaudeFunctionParameter( - string? name, - string? description, - bool isRequired, - Type? parameterType, - KernelJsonSchema? schema) - { - this.Name = name ?? string.Empty; - this.Description = description ?? string.Empty; - this.IsRequired = isRequired; - this.ParameterType = parameterType; - this.Schema = schema; - } - - /// Gets the name of the parameter. - public string Name { get; } - - /// Gets a description of the parameter. - public string Description { get; } - - /// Gets whether the parameter is required vs optional. - public bool IsRequired { get; } - - /// Gets the of the parameter, if known. - public Type? ParameterType { get; } - - /// Gets a JSON schema for the parameter, if known. - public KernelJsonSchema? Schema { get; } -} - -/// -/// Represents a function return parameter that can be returned by a tool call to Anthropic. -/// -public sealed class ClaudeFunctionReturnParameter -{ - internal ClaudeFunctionReturnParameter( - string? description, - Type? parameterType, - KernelJsonSchema? schema) - { - this.Description = description ?? string.Empty; - this.Schema = schema; - this.ParameterType = parameterType; - } - - /// Gets a description of the return parameter. - public string Description { get; } - - /// Gets the of the return parameter, if known. - public Type? ParameterType { get; } - - /// Gets a JSON schema for the return parameter, if known. - public KernelJsonSchema? Schema { get; } -} - -/// -/// Represents a function that can be passed to the Anthropic API -/// -public sealed class AnthropicFunction -{ - /// - /// Cached schema for a description less string. - /// - private static readonly KernelJsonSchema s_stringNoDescriptionSchema = KernelJsonSchema.Parse("{\"type\":\"string\"}"); - - /// Initializes the . - internal AnthropicFunction( - string? pluginName, - string functionName, - string? description, - IReadOnlyList? parameters, - ClaudeFunctionReturnParameter? returnParameter) - { - Verify.NotNullOrWhiteSpace(functionName); - - this.PluginName = pluginName; - this.FunctionName = functionName; - this.Description = description; - this.Parameters = parameters; - this.ReturnParameter = returnParameter; - } - - /// Gets the separator used between the plugin name and the function name, if a plugin name is present. - /// Default is _
It can't be -, because Anthropic truncates the plugin name if a dash is used
- public static string NameSeparator { get; set; } = "_"; - - /// Gets the name of the plugin with which the function is associated, if any. - public string? PluginName { get; } - - /// Gets the name of the function. - public string FunctionName { get; } - - /// Gets the fully-qualified name of the function. - /// - /// This is the concatenation of the and the , - /// separated by . If there is no , this is - /// the same as . - /// - public string FullyQualifiedName => - string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{NameSeparator}{this.FunctionName}"; - - /// Gets a description of the function. - public string? Description { get; } - - /// Gets a list of parameters to the function, if any. - public IReadOnlyList? Parameters { get; } - - /// Gets the return parameter of the function, if any. - public ClaudeFunctionReturnParameter? ReturnParameter { get; } - - /// - /// Converts the representation to the Anthropic API's - /// representation. - /// - /// A containing all the function information. - internal AnthropicToolFunctionDeclaration ToFunctionDeclaration() - { - Dictionary? resultParameters = null; - - if (this.Parameters is { Count: > 0 }) - { - var properties = new Dictionary(); - var required = new List(); - - foreach (var parameter in this.Parameters) - { - properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForParameter(parameter)); - if (parameter.IsRequired) - { - required.Add(parameter.Name); - } - } - - resultParameters = new Dictionary - { - { "type", "object" }, - { "required", required }, - { "properties", properties }, - }; - } - - return new AnthropicToolFunctionDeclaration - { - Name = this.FullyQualifiedName, - Description = this.Description ?? throw new InvalidOperationException( - $"Function description is required. Please provide a description for the function {this.FullyQualifiedName}."), - Parameters = JsonSerializer.SerializeToNode(resultParameters), - }; - } - - /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) - private static KernelJsonSchema GetDefaultSchemaForParameter(ClaudeFunctionParameter parameter) - { - // If there's a description, incorporate it. - if (!string.IsNullOrWhiteSpace(parameter.Description)) - { - return KernelJsonSchemaBuilder.Build(null, parameter.ParameterType ?? typeof(string), parameter.Description); - } - - // Otherwise, we can use a cached schema for a string with no description. - return s_stringNoDescriptionSchema; - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs deleted file mode 100644 index e59cb387ae2a..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolCall.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.Json; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic; - -/// -/// Represents an Anthropic function tool call with deserialized function name and arguments. -/// -public sealed class AnthropicFunctionToolCall -{ - private string? _fullyQualifiedFunctionName; - - /// Initialize the from a . - internal AnthropicFunctionToolCall(AnthropicToolCallContent functionToolCall) - { - Verify.NotNull(functionToolCall); - Verify.NotNull(functionToolCall.FunctionName); - - string fullyQualifiedFunctionName = functionToolCall.FunctionName; - string functionName = fullyQualifiedFunctionName; - string? pluginName = null; - - int separatorPos = fullyQualifiedFunctionName.IndexOf(AnthropicFunction.NameSeparator, StringComparison.Ordinal); - if (separatorPos >= 0) - { - pluginName = fullyQualifiedFunctionName.AsSpan(0, separatorPos).Trim().ToString(); - functionName = fullyQualifiedFunctionName.AsSpan(separatorPos + AnthropicFunction.NameSeparator.Length).Trim().ToString(); - } - - this._fullyQualifiedFunctionName = fullyQualifiedFunctionName; - this.ToolUseId = functionToolCall.ToolId; - this.PluginName = pluginName; - this.FunctionName = functionName; - if (functionToolCall.Arguments is not null) - { - this.Arguments = functionToolCall.Arguments.Deserialize>(); - } - } - - /// - /// The id of tool returned by the claude. - /// - public string ToolUseId { get; } - - /// Gets the name of the plugin with which this function is associated, if any. - public string? PluginName { get; } - - /// Gets the name of the function. - public string FunctionName { get; } - - /// Gets a name/value collection of the arguments to the function, if any. - public IReadOnlyDictionary? Arguments { get; } - - /// Gets the fully-qualified name of the function. - /// - /// This is the concatenation of the and the , - /// separated by . If there is no , - /// this is the same as . - /// - public string FullyQualifiedName - => this._fullyQualifiedFunctionName - ??= string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{AnthropicFunction.NameSeparator}{this.FunctionName}"; - - /// - public override string ToString() - { - var sb = new StringBuilder(this.FullyQualifiedName); - - sb.Append('('); - if (this.Arguments is not null) - { - string separator = ""; - foreach (var arg in this.Arguments) - { - sb.Append(separator).Append(arg.Key).Append(':').Append(arg.Value); - separator = ", "; - } - } - - sb.Append(')'); - - return sb.ToString(); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolResult.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolResult.cs deleted file mode 100644 index cf8157bcc2a5..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFunctionToolResult.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Connectors.Anthropic; - -/// -/// Represents the result of a Claude function tool call. -/// -public sealed class AnthropicFunctionToolResult -{ - /// - /// Initializes a new instance of the class. - /// - /// The called function. - /// The result of the function. - /// The id of tool returned by the claude. - public AnthropicFunctionToolResult(AnthropicFunctionToolCall toolCall, FunctionResult functionResult, string? toolUseId) - { - Verify.NotNull(toolCall); - Verify.NotNull(functionResult); - - this.FunctionResult = functionResult; - this.FullyQualifiedName = toolCall.FullyQualifiedName; - this.ToolUseId = toolUseId; - } - - /// - /// Gets the result of the function. - /// - public FunctionResult FunctionResult { get; } - - /// Gets the fully-qualified name of the function. - /// ClaudeFunctionToolCall.FullyQualifiedName - public string FullyQualifiedName { get; } - - /// - /// The id of tool returned by the claude. - /// - public string? ToolUseId { get; } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index 0f94fafc82e1..33aee583206d 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -62,7 +62,7 @@ public AnthropicChatCompletionService( public AnthropicChatCompletionService( string modelId, Uri endpoint, - Func? requestHandler, + Func? requestHandler, AnthropicClientOptions? options = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) From 641aa2c6088618255f922df33624f188669f2c5d Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Thu, 4 Jul 2024 21:10:32 +0200 Subject: [PATCH 04/19] Added integration tests for chat completion. --- .../Anthropic/AnthropicChatCompletionTests.cs | 266 ++++++++++++++++++ .../Connectors/Anthropic/TestsBase.cs | 42 +++ .../IntegrationTests/IntegrationTests.csproj | 1 + 3 files changed, 309 insertions(+) create mode 100644 dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs new file mode 100644 index 000000000000..0a71a5b4abae --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.Anthropic; +using xRetry; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; + +public sealed class AnthropicChatCompletionTests(ITestOutputHelper output) : TestsBase(output) +{ + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatGenerationReturnsValidResponseAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and expand this abbreviation: LLM"); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + Assert.NotNull(response.Content); + this.Output.WriteLine(response.Content); + Assert.Contains("Large Language Model", response.Content, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Brandon", response.Content, StringComparison.OrdinalIgnoreCase); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatStreamingReturnsValidResponseAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and write a long story about my name."); + + var sut = this.GetChatService(serviceType); + + // Act + var response = + await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + Assert.NotEmpty(response); + Assert.True(response.Count > 1); + var message = string.Concat(response.Select(c => c.Content)); + Assert.False(string.IsNullOrWhiteSpace(message)); + this.Output.WriteLine(message); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatGenerationVisionBinaryDataAsync(ServiceType serviceType) + { + // Arrange + Memory image = await File.ReadAllBytesAsync("./TestData/test_image_001.jpg"); + var chatHistory = new ChatHistory(); + var messageContent = new ChatMessageContent(AuthorRole.User, items: + [ + new TextContent("This is an image with a car. Which color is it? You can chose from red, blue, green, and yellow"), + new ImageContent(image, "image/jpeg") + ]); + chatHistory.Add(messageContent); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + Assert.NotNull(response.Content); + this.Output.WriteLine(response.Content); + Assert.Contains("green", response.Content, StringComparison.OrdinalIgnoreCase); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatStreamingVisionBinaryDataAsync(ServiceType serviceType) + { + // Arrange + Memory image = await File.ReadAllBytesAsync("./TestData/test_image_001.jpg"); + var chatHistory = new ChatHistory(); + var messageContent = new ChatMessageContent(AuthorRole.User, items: + [ + new TextContent("This is an image with a car. Which color is it? You can chose from red, blue, green, and yellow"), + new ImageContent(image, "image/jpeg") + ]); + chatHistory.Add(messageContent); + + var sut = this.GetChatService(serviceType); + + // Act + var responses = await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + Assert.NotEmpty(responses); + var message = string.Concat(responses.Select(c => c.Content)); + Assert.False(string.IsNullOrWhiteSpace(message)); + this.Output.WriteLine(message); + Assert.Contains("green", message, StringComparison.OrdinalIgnoreCase); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test needs setup first.")] + public async Task ChatGenerationVisionUriAsync(ServiceType serviceType) + { + // Arrange + Uri imageUri = new("gs://generativeai-downloads/images/scones.jpg"); // needs setup + var chatHistory = new ChatHistory(); + var messageContent = new ChatMessageContent(AuthorRole.User, items: + [ + new TextContent("This is an image with a car. Which color is it? You can chose from red, blue, green, and yellow"), + new ImageContent(imageUri) { MimeType = "image/jpeg" } + ]); + chatHistory.Add(messageContent); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + Assert.NotNull(response.Content); + this.Output.WriteLine(response.Content); + Assert.Contains("green", response.Content, StringComparison.OrdinalIgnoreCase); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test needs setup first.")] + public async Task ChatStreamingVisionUriAsync(ServiceType serviceType) + { + // Arrange + Uri imageUri = new("gs://generativeai-downloads/images/scones.jpg"); // needs setup + var chatHistory = new ChatHistory(); + var messageContent = new ChatMessageContent(AuthorRole.User, items: + [ + new TextContent("This is an image with a car. Which color is it? You can chose from red, blue, green, and yellow"), + new ImageContent(imageUri) { MimeType = "image/jpeg" } + ]); + chatHistory.Add(messageContent); + + var sut = this.GetChatService(serviceType); + + // Act + var responses = await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + Assert.NotEmpty(responses); + var message = string.Concat(responses.Select(c => c.Content)); + Assert.False(string.IsNullOrWhiteSpace(message)); + this.Output.WriteLine(message); + Assert.Contains("green", message, StringComparison.OrdinalIgnoreCase); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatGenerationReturnsUsedTokensAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and expand this abbreviation: LLM"); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + var metadata = response.Metadata as AnthropicMetadata; + Assert.NotNull(metadata); + foreach ((string? key, object? value) in metadata) + { + this.Output.WriteLine($"{key}: {JsonSerializer.Serialize(value)}"); + } + + Assert.True(metadata.TotalTokenCount > 0); + Assert.True(metadata.InputTokenCount > 0); + Assert.True(metadata.OutputTokenCount > 0); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatStreamingReturnsUsedTokensAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and expand this abbreviation: LLM"); + + var sut = this.GetChatService(serviceType); + + // Act + var responses = await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + var metadata = responses.Last().Metadata as AnthropicMetadata; + Assert.NotNull(metadata); + this.Output.WriteLine($"TotalTokenCount: {metadata.TotalTokenCount}"); + this.Output.WriteLine($"InputTokenCount: {metadata.InputTokenCount}"); + this.Output.WriteLine($"OutputTokenCount: {metadata.OutputTokenCount}"); + Assert.True(metadata.TotalTokenCount > 0); + Assert.True(metadata.InputTokenCount > 0); + Assert.True(metadata.OutputTokenCount > 0); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatGenerationReturnsStopFinishReasonAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and expand this abbreviation: LLM"); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + var metadata = response.Metadata as AnthropicMetadata; + Assert.NotNull(metadata); + this.Output.WriteLine($"FinishReason: {metadata.FinishReason}"); + Assert.Equal(AnthropicFinishReason.Stop, metadata.FinishReason); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + public async Task ChatStreamingReturnsStopFinishReasonAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and expand this abbreviation: LLM"); + + var sut = this.GetChatService(serviceType); + + // Act + var responses = await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + var metadata = responses.Last().Metadata as AnthropicMetadata; + Assert.NotNull(metadata); + this.Output.WriteLine($"FinishReason: {metadata.FinishReason}"); + Assert.Equal(AnthropicFinishReason.Stop, metadata.FinishReason); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs new file mode 100644 index 000000000000..d3d2549f586b --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.Anthropic; +using Xunit.Abstractions; + +namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; + +public abstract class TestsBase(ITestOutputHelper output) +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + + protected ITestOutputHelper Output { get; } = output; + + protected IChatCompletionService GetChatService(ServiceType serviceType) => serviceType switch + { + ServiceType.Anthropic => new AnthropicChatCompletionService( + modelId: this.AnthropicGetModel(), + apiKey: this.AnthropicGetApiKey()), + ServiceType.VertexAI => throw new NotImplementedException("Implement in next PR"), // TODO: Implement in next PR + _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) + }; + + public enum ServiceType + { + Anthropic, + VertexAI + } + + private string AnthropicGetModel() => this._configuration.GetSection("Anthropic:ModelId").Get()!; + private string AnthropicGetApiKey() => this._configuration.GetSection("Anthropic:ApiKey").Get()!; + private string VertexAIGetModel() => this._configuration.GetSection("VertexAI:Anthropic:ModelId").Get()!; + private string VertexAIGetEndpoint() => this._configuration.GetSection("VertexAI:Anthropic:Endpoint").Get()!; + private string VertexAIGetBearerKey() => this._configuration.GetSection("VertexAI:BearerKey").Get()!; +} diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index df5afa473ce7..8a7ae84bacef 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -59,6 +59,7 @@ + From 6ea8340c252fadd64a7fd064b6dc2a254b9f3bda Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Thu, 4 Jul 2024 21:22:46 +0200 Subject: [PATCH 05/19] format --- .../Core/AnthropicChatGenerationTests.cs | 1 - .../Core/AnthropicRequestTests.cs | 1 - .../AnthropicPromptExecutionSettings.cs | 1 - .../Core/Models/Message/AnthropicContent.cs | 2 -- .../Core/Models/Message/TypeJsonDyscriminator.cs | 4 ++-- .../Models/AnthropicChatMessageContent.cs | 6 ++---- 6 files changed, 4 insertions(+), 11 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index b6d5c70d6cfb..7d5aa6e15f6f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs index 5bd0d70a03b5..4bcb7aacaa0f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Nodes; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs index 482586a23794..4e0b711df534 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs @@ -5,7 +5,6 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.Anthropic; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index dea8a7554054..2cfcbe9996fb 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text.Json.Serialization; - namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs index ff76a5bc14ae..09941962f855 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs @@ -50,8 +50,8 @@ internal static class JsonTypeDiscriminatorHelper [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] internal sealed class HackyJsonDerivedAttribute(Type subtype, string typeDiscriminator) : Attribute { - public Type Subtype { get; set; } = subtype; - public string TypeDiscriminator { get; set; } = typeDiscriminator; + public Type Subtype { get; internal set; } = subtype; + public string TypeDiscriminator { get; internal set; } = typeDiscriminator; } internal sealed class PolymorphicJsonConverterFactory : JsonConverterFactory diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs index 6788fc8da7f4..3e33d751fb60 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs @@ -1,11 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.Json.Serialization; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; namespace Microsoft.SemanticKernel.Connectors.Anthropic; @@ -40,7 +37,8 @@ internal AnthropicChatMessageContent( modelId: modelId, innerContent: innerContent, encoding: Encoding.UTF8, - metadata: metadata) { } + metadata: metadata) + { } /// /// The metadata associated with the content. From 0829c7f1888419a2dc268b729f719b5180bd5d6b Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Thu, 4 Jul 2024 22:23:58 +0200 Subject: [PATCH 06/19] Options update to support vertex and aws (not tested) --- .../Core/AnthropicChatGenerationTests.cs | 36 +++++++-- ...thropicServiceCollectionExtensionsTests.cs | 9 ++- .../AnthropicClientOptions.cs | 80 +++++++++++++++++-- .../Core/AnthropicClient.cs | 22 +++-- .../Core/Models/AnthropicRequest.cs | 3 + .../AnthropicKernelBuilderExtensions.cs | 10 ++- .../AnthropicServiceCollectionExtensions.cs | 10 ++- .../AnthropicChatCompletionService.cs | 10 ++- .../Anthropic/AnthropicChatCompletionTests.cs | 22 ++++- .../Anthropic/{TestsBase.cs => TestBase.cs} | 25 +++++- .../Google/EmbeddingGenerationTests.cs | 2 +- .../Gemini/GeminiChatCompletionTests.cs | 2 +- .../Gemini/GeminiFunctionCallingTests.cs | 2 +- .../Google/{TestsBase.cs => TestBase.cs} | 4 +- 14 files changed, 193 insertions(+), 44 deletions(-) rename dotnet/src/IntegrationTests/Connectors/Anthropic/{TestsBase.cs => TestBase.cs} (57%) rename dotnet/src/IntegrationTests/Connectors/Google/{TestsBase.cs => TestBase.cs} (97%) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index 7d5aa6e15f6f..9de777245e3f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -222,6 +222,28 @@ public async Task ShouldPassSystemMessageToRequestAsync() Assert.All(messages, msg => Assert.Contains(msg, request.SystemPrompt, StringComparison.OrdinalIgnoreCase)); } + [Fact] + public async Task ShouldPassVersionToRequestBodyIfCustomHandlerUsedAsync() + { + // Arrange + var options = new AnthropicClientOptions(); + var client = new AnthropicClient( + httpClient: this._httpClient, + modelId: "fake-model", + options: new AnthropicClientOptions(), + endpoint: new Uri("https://fake-uri.com"), + requestHandler: _ => ValueTask.CompletedTask); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicRequest? request = Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Equal(options.Version, request.Version); + } + [Fact] public async Task ShouldThrowArgumentExceptionIfChatHistoryIsEmptyAsync() { @@ -266,7 +288,7 @@ public async Task ItCreatesPostRequestAsync() } [Fact] - public async Task ItCreatesPostRequestWithValidUserAgentAsync() + public async Task ItCreatesRequestWithValidUserAgentAsync() { // Arrange var client = this.CreateChatCompletionClient(); @@ -281,7 +303,7 @@ public async Task ItCreatesPostRequestWithValidUserAgentAsync() } [Fact] - public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync() + public async Task ItCreatesRequestWithSemanticKernelVersionHeaderAsync() { // Arrange var client = this.CreateChatCompletionClient(); @@ -299,7 +321,7 @@ public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync() } [Fact] - public async Task ItCreatesPostRequestWithValidAnthropicVersionAsync() + public async Task ItCreatesRequestWithValidAnthropicVersionAsync() { // Arrange var options = new AnthropicClientOptions(); @@ -315,7 +337,7 @@ public async Task ItCreatesPostRequestWithValidAnthropicVersionAsync() } [Fact] - public async Task ItCreatesPostRequestWithValidApiKeyAsync() + public async Task ItCreatesRequestWithValidApiKeyAsync() { // Arrange string apiKey = "fake-claude-key"; @@ -331,7 +353,7 @@ public async Task ItCreatesPostRequestWithValidApiKeyAsync() } [Fact] - public async Task ItCreatesPostRequestWithJsonContentTypeAsync() + public async Task ItCreatesRequestWithJsonContentTypeAsync() { // Arrange var client = this.CreateChatCompletionClient(); @@ -347,7 +369,7 @@ public async Task ItCreatesPostRequestWithJsonContentTypeAsync() } [Fact] - public async Task ItCreatesPostRequestWithCustomUriAndCustomHeadersAsync() + public async Task ItCreatesRequestWithCustomUriAndCustomHeadersAsync() { // Arrange Uri uri = new("https://fake-uri.com"); @@ -360,7 +382,7 @@ ValueTask RequestHandler(HttpRequestMessage arg) var client = new AnthropicClient( httpClient: this._httpClient, modelId: "fake-model", - options: null, + options: new AnthropicClientOptions(), endpoint: uri, requestHandler: RequestHandler); diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs index 69b79a5d9283..80271f97f94d 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; @@ -53,7 +54,9 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInKern var kernelBuilder = Kernel.CreateBuilder(); // Act - kernelBuilder.AddAnthropicChatCompletion("modelId", new Uri("https://example.com"), null); + kernelBuilder.AddAnthropicChatCompletion( + "modelId", new Uri("https://example.com"), + _ => ValueTask.CompletedTask, new AnthropicClientOptions()); var kernel = kernelBuilder.Build(); // Assert @@ -69,7 +72,9 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInServ var services = new ServiceCollection(); // Act - services.AddAnthropicChatCompletion("modelId", new Uri("https://example.com"), null); + services.AddAnthropicChatCompletion( + "modelId", new Uri("https://example.com"), + _ => ValueTask.CompletedTask, new AnthropicClientOptions()); var serviceProvider = services.BuildServiceProvider(); // Assert diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs index 19ad4e1b4158..6d469ff1e3c5 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs @@ -5,18 +5,25 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; #pragma warning disable CA1707 // Identifiers should not contain underscores +#pragma warning disable CA1008 // Enums should have zero value /// /// Represents the options for configuring the Anthropic client. /// -public sealed class AnthropicClientOptions +public abstract class ClientOptions +{ + internal string Version { get; private protected init; } = null!; +} + +/// +/// Represents the options for configuring the Anthropic client with Anthropic provider. +/// +public sealed class AnthropicClientOptions : ClientOptions { private const ServiceVersion LatestVersion = ServiceVersion.V2023_06_01; /// The version of the service to use. -#pragma warning disable CA1008 // Enums should have zero value public enum ServiceVersion -#pragma warning restore CA1008 { /// Service version "2023-01-01". V2023_01_01 = 1, @@ -25,15 +32,12 @@ public enum ServiceVersion V2023_06_01 = 2, } - internal string Version { get; } - /// /// Initializes new instance of /// /// /// This parameter is optional. /// Default value is .
- /// The version is ignored when used with other vendor than Anthropic. /// /// Provided version is not supported. public AnthropicClientOptions(ServiceVersion version = LatestVersion) @@ -46,3 +50,67 @@ public AnthropicClientOptions(ServiceVersion version = LatestVersion) }; } } + +/// +/// Represents the options for configuring the Anthropic client with Google VertexAI provider. +/// +public sealed class VertexAIAnthropicClientOptions : ClientOptions +{ + private const ServiceVersion LatestVersion = ServiceVersion.V2023_10_16; + + /// The version of the service to use. + public enum ServiceVersion + { + /// Service version "vertex-2023-10-16". + V2023_10_16 = 1, + } + + /// + /// Initializes new instance of + /// + /// + /// This parameter is optional. + /// Default value is .
+ /// + /// Provided version is not supported. + public VertexAIAnthropicClientOptions(ServiceVersion version = LatestVersion) + { + this.Version = version switch + { + ServiceVersion.V2023_10_16 => "vertex-2023-10-16", + _ => throw new NotSupportedException("Unsupported service version") + }; + } +} + +/// +/// Represents the options for configuring the Anthropic client with Amazon Bedrock provider. +/// +public sealed class AmazonBedrockAnthropicClientOptions : ClientOptions +{ + private const ServiceVersion LatestVersion = ServiceVersion.V2023_05_31; + + /// The version of the service to use. + public enum ServiceVersion + { + /// Service version "bedrock-2023-05-31". + V2023_05_31 = 1, + } + + /// + /// Initializes new instance of + /// + /// + /// This parameter is optional. + /// Default value is .
+ /// + /// Provided version is not supported. + public AmazonBedrockAnthropicClientOptions(ServiceVersion version = LatestVersion) + { + this.Version = version switch + { + ServiceVersion.V2023_05_31 => "bedrock-2023-05-31", + _ => throw new NotSupportedException("Unsupported service version") + }; + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index 1e77a05a6b18..1fa1e105bbef 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -40,7 +40,7 @@ internal sealed class AnthropicClient private readonly string? _apiKey; private readonly Uri _endpoint; private readonly Func? _customRequestHandler; - private readonly AnthropicClientOptions _options; + private readonly ClientOptions _options; private static readonly string s_namespace = typeof(AnthropicChatCompletionService).Namespace!; @@ -88,7 +88,7 @@ public AnthropicClient( HttpClient httpClient, string modelId, string apiKey, - AnthropicClientOptions? options, + ClientOptions? options, ILogger? logger = null) { Verify.NotNull(httpClient); @@ -116,20 +116,22 @@ public AnthropicClient( HttpClient httpClient, string modelId, Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options, + Func requestHandler, + ClientOptions options, ILogger? logger = null) { Verify.NotNull(httpClient); Verify.NotNullOrWhiteSpace(modelId); Verify.NotNull(endpoint); + Verify.NotNull(requestHandler); + Verify.NotNull(options); this._httpClient = httpClient; this._logger = logger ?? NullLogger.Instance; this._modelId = modelId; this._endpoint = endpoint; this._customRequestHandler = requestHandler; - this._options = options ?? new AnthropicClientOptions(); + this._options = options; } /// @@ -263,11 +265,17 @@ private ChatCompletionState ValidateInputAndCreateChatCompletionState( JsonSerializer.Serialize(anthropicExecutionSettings)); var filteredChatHistory = new ChatHistory(chatHistory.Where(IsAssistantOrUserOrSystem)); - return new ChatCompletionState() + var anthropicRequest = AnthropicRequest.FromChatHistoryAndExecutionSettings(filteredChatHistory, anthropicExecutionSettings); + if (this._customRequestHandler != null) + { + anthropicRequest.Version = this._options.Version; + } + + return new ChatCompletionState { ChatHistory = chatHistory, ExecutionSettings = anthropicExecutionSettings, - AnthropicRequest = AnthropicRequest.FromChatHistoryAndExecutionSettings(filteredChatHistory, anthropicExecutionSettings) + AnthropicRequest = anthropicRequest }; static bool IsAssistantOrUserOrSystem(ChatMessageContent msg) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index eb4b5307bee6..844c32213650 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -11,6 +11,9 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; internal sealed class AnthropicRequest { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; set; } + /// /// Input messages.
/// Our models are trained to operate on alternating user and assistant conversational turns. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index e0f361bc50dd..a4085a461d94 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -30,7 +30,7 @@ public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, string modelId, string apiKey, - AnthropicClientOptions? options = null, + ClientOptions? options = null, string? serviceId = null, HttpClient? httpClient = null) { @@ -55,7 +55,7 @@ public static IKernelBuilder AddAnthropicChatCompletion( /// The model for chat completion. /// Endpoint for the chat completion model /// A custom request handler to be used for sending HTTP requests - /// Optional options for the anthropic client + /// Options for the anthropic client /// The optional service ID. /// The optional custom HttpClient. /// The updated kernel builder. @@ -63,14 +63,16 @@ public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, string modelId, Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options = null, + Func requestHandler, + ClientOptions options, string? serviceId = null, HttpClient? httpClient = null) { Verify.NotNull(builder); Verify.NotNull(modelId); Verify.NotNull(endpoint); + Verify.NotNull(options); + Verify.NotNull(requestHandler); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 1852f8d3f52e..84da12cc4a0e 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -29,7 +29,7 @@ public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, string modelId, string apiKey, - AnthropicClientOptions? options = null, + ClientOptions? options = null, string? serviceId = null) { Verify.NotNull(services); @@ -53,20 +53,22 @@ public static IServiceCollection AddAnthropicChatCompletion( /// The model for chat completion. /// Endpoint for the chat completion model /// A custom request handler to be used for sending HTTP requests - /// Optional options for the anthropic client + /// Options for the anthropic client /// Optional service ID. /// The updated service collection. public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, string modelId, Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options = null, + Func requestHandler, + ClientOptions options, string? serviceId = null) { Verify.NotNull(services); Verify.NotNull(modelId); Verify.NotNull(endpoint); + Verify.NotNull(requestHandler); + Verify.NotNull(options); services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index 33aee583206d..d25127a80a32 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -32,7 +32,7 @@ public sealed class AnthropicChatCompletionService : IChatCompletionService public AnthropicChatCompletionService( string modelId, string apiKey, - AnthropicClientOptions? options = null, + ClientOptions? options = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { @@ -56,19 +56,21 @@ public AnthropicChatCompletionService( /// The model for the chat completion service. /// Endpoint for the chat completion model /// A custom request handler to be used for sending HTTP requests - /// Optional options for the anthropic client + /// Options for the anthropic client /// Optional HTTP client to be used for communication with the Claude API. /// Optional logger factory to be used for logging. public AnthropicChatCompletionService( string modelId, Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options = null, + Func requestHandler, + ClientOptions options, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { Verify.NotNullOrWhiteSpace(modelId); Verify.NotNull(endpoint); + Verify.NotNull(options); + Verify.NotNull(requestHandler); this._client = new AnthropicClient( #pragma warning disable CA2000 diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs index 0a71a5b4abae..5e09060eefa0 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs @@ -14,10 +14,12 @@ namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; -public sealed class AnthropicChatCompletionTests(ITestOutputHelper output) : TestsBase(output) +public sealed class AnthropicChatCompletionTests(ITestOutputHelper output) : TestBase(output) { [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatGenerationReturnsValidResponseAsync(ServiceType serviceType) { // Arrange @@ -40,6 +42,8 @@ public async Task ChatGenerationReturnsValidResponseAsync(ServiceType serviceTyp [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatStreamingReturnsValidResponseAsync(ServiceType serviceType) { // Arrange @@ -64,6 +68,8 @@ public async Task ChatStreamingReturnsValidResponseAsync(ServiceType serviceType [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatGenerationVisionBinaryDataAsync(ServiceType serviceType) { // Arrange @@ -89,6 +95,8 @@ public async Task ChatGenerationVisionBinaryDataAsync(ServiceType serviceType) [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatStreamingVisionBinaryDataAsync(ServiceType serviceType) { // Arrange @@ -116,6 +124,8 @@ public async Task ChatStreamingVisionBinaryDataAsync(ServiceType serviceType) [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test needs setup first.")] + [InlineData(ServiceType.VertexAI, Skip = "This test needs setup first.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test needs setup first.")] public async Task ChatGenerationVisionUriAsync(ServiceType serviceType) { // Arrange @@ -141,6 +151,8 @@ public async Task ChatGenerationVisionUriAsync(ServiceType serviceType) [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test needs setup first.")] + [InlineData(ServiceType.VertexAI, Skip = "This test needs setup first.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test needs setup first.")] public async Task ChatStreamingVisionUriAsync(ServiceType serviceType) { // Arrange @@ -168,6 +180,8 @@ public async Task ChatStreamingVisionUriAsync(ServiceType serviceType) [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatGenerationReturnsUsedTokensAsync(ServiceType serviceType) { // Arrange @@ -196,6 +210,8 @@ public async Task ChatGenerationReturnsUsedTokensAsync(ServiceType serviceType) [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatStreamingReturnsUsedTokensAsync(ServiceType serviceType) { // Arrange @@ -222,6 +238,8 @@ public async Task ChatStreamingReturnsUsedTokensAsync(ServiceType serviceType) [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatGenerationReturnsStopFinishReasonAsync(ServiceType serviceType) { // Arrange @@ -244,6 +262,8 @@ public async Task ChatGenerationReturnsStopFinishReasonAsync(ServiceType service [RetryTheory] [InlineData(ServiceType.Anthropic, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.VertexAI, Skip = "This test is for manual verification.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This test is for manual verification.")] public async Task ChatStreamingReturnsStopFinishReasonAsync(ServiceType serviceType) { // Arrange diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs similarity index 57% rename from dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs rename to dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs index d3d2549f586b..8b51dc55c2d2 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestsBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; @@ -8,12 +9,12 @@ namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; -public abstract class TestsBase(ITestOutputHelper output) +public abstract class TestBase(ITestOutputHelper output) { private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddUserSecrets() + .AddUserSecrets() .AddEnvironmentVariables() .Build(); @@ -24,14 +25,28 @@ public abstract class TestsBase(ITestOutputHelper output) ServiceType.Anthropic => new AnthropicChatCompletionService( modelId: this.AnthropicGetModel(), apiKey: this.AnthropicGetApiKey()), - ServiceType.VertexAI => throw new NotImplementedException("Implement in next PR"), // TODO: Implement in next PR + ServiceType.VertexAI => new AnthropicChatCompletionService( + modelId: this.VertexAIGetModel(), + endpoint: new Uri(this.VertexAIGetEndpoint()), + options: new VertexAIAnthropicClientOptions(), + requestHandler: requestMessage => + { + requestMessage.Headers.Authorization = new("Bearer", this.VertexAIGetBearerKey()); + return ValueTask.CompletedTask; + }), + ServiceType.AmazonBedrock => new AnthropicChatCompletionService( + modelId: this.AmazonBedrockGetModel(), + endpoint: new Uri(this.AmazonBedrockGetEndpoint()), + options: new AmazonBedrockAnthropicClientOptions(), + requestHandler: _ => throw new NotImplementedException("setup later")), // TODO: setup aws bedrock claude _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; public enum ServiceType { Anthropic, - VertexAI + VertexAI, + AmazonBedrock } private string AnthropicGetModel() => this._configuration.GetSection("Anthropic:ModelId").Get()!; @@ -39,4 +54,6 @@ public enum ServiceType private string VertexAIGetModel() => this._configuration.GetSection("VertexAI:Anthropic:ModelId").Get()!; private string VertexAIGetEndpoint() => this._configuration.GetSection("VertexAI:Anthropic:Endpoint").Get()!; private string VertexAIGetBearerKey() => this._configuration.GetSection("VertexAI:BearerKey").Get()!; + private string AmazonBedrockGetModel() => this._configuration.GetSection("AmazonBedrock:Anthropic:ModelId").Get()!; + private string AmazonBedrockGetEndpoint() => this._configuration.GetSection("AmazonBedrock:Anthropic:Endpoint").Get()!; } diff --git a/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs index 79fc5db80aff..a3b4716174db 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/EmbeddingGenerationTests.cs @@ -8,7 +8,7 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google; -public sealed class EmbeddingGenerationTests(ITestOutputHelper output) : TestsBase(output) +public sealed class EmbeddingGenerationTests(ITestOutputHelper output) : TestBase(output) { [RetryTheory] [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs index 321ede0ff115..098e41e9d7fa 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs @@ -14,7 +14,7 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini; -public sealed class GeminiChatCompletionTests(ITestOutputHelper output) : TestsBase(output) +public sealed class GeminiChatCompletionTests(ITestOutputHelper output) : TestBase(output) { [RetryTheory] [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingTests.cs index 37c48f0842b4..53629fe191da 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiFunctionCallingTests.cs @@ -14,7 +14,7 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google.Gemini; -public sealed class GeminiFunctionCallingTests(ITestOutputHelper output) : TestsBase(output) +public sealed class GeminiFunctionCallingTests(ITestOutputHelper output) : TestBase(output) { [RetryTheory] [InlineData(ServiceType.GoogleAI, Skip = "This test is for manual verification.")] diff --git a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Google/TestBase.cs similarity index 97% rename from dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs rename to dotnet/src/IntegrationTests/Connectors/Google/TestBase.cs index 6b932727f4a6..8cf794d473b1 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/TestBase.cs @@ -9,12 +9,12 @@ namespace SemanticKernel.IntegrationTests.Connectors.Google; -public abstract class TestsBase(ITestOutputHelper output) +public abstract class TestBase(ITestOutputHelper output) { private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddUserSecrets() + .AddUserSecrets() .AddEnvironmentVariables() .Build(); From 03a229a4abc0045220bc1a44e1276ff73421f77d Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Fri, 5 Jul 2024 17:53:50 +0200 Subject: [PATCH 07/19] Added additional ITs --- .../Anthropic/AnthropicChatCompletionTests.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs index 5e09060eefa0..6e791d7aa5f9 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs @@ -283,4 +283,96 @@ public async Task ChatStreamingReturnsStopFinishReasonAsync(ServiceType serviceT this.Output.WriteLine($"FinishReason: {metadata.FinishReason}"); Assert.Equal(AnthropicFinishReason.Stop, metadata.FinishReason); } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.VertexAI, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This can fail. Anthropic does not support this feature yet.")] + public async Task ChatGenerationOnlyAssistantMessagesAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddAssistantMessage("I'm very thirsty."); + chatHistory.AddAssistantMessage("Could you give me a glass of..."); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + string[] words = ["water", "juice", "milk", "soda", "tea", "coffee", "beer", "wine"]; + this.Output.WriteLine(response.Content); + Assert.Contains(words, word => response.Content!.Contains(word, StringComparison.OrdinalIgnoreCase)); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.VertexAI, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This can fail. Anthropic does not support this feature yet.")] + public async Task ChatStreamingOnlyAssistantMessagesAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddAssistantMessage("I'm very thirsty."); + chatHistory.AddAssistantMessage("Could you give me a glass of..."); + + var sut = this.GetChatService(serviceType); + + // Act + var responses = await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + string[] words = ["water", "juice", "milk", "soda", "tea", "coffee", "beer", "wine"]; + Assert.NotEmpty(responses); + var message = string.Concat(responses.Select(c => c.Content)); + this.Output.WriteLine(message); + Assert.Contains(words, word => message.Contains(word, StringComparison.OrdinalIgnoreCase)); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.VertexAI, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This can fail. Anthropic does not support this feature yet.")] + public async Task ChatGenerationOnlyUserMessagesAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("I'm very thirsty."); + chatHistory.AddUserMessage("Could you give me a glass of..."); + + var sut = this.GetChatService(serviceType); + + // Act + var response = await sut.GetChatMessageContentAsync(chatHistory); + + // Assert + string[] words = ["water", "juice", "milk", "soda", "tea", "coffee", "beer", "wine"]; + this.Output.WriteLine(response.Content); + Assert.Contains(words, word => response.Content!.Contains(word, StringComparison.OrdinalIgnoreCase)); + } + + [RetryTheory] + [InlineData(ServiceType.Anthropic, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.VertexAI, Skip = "This can fail. Anthropic does not support this feature yet.")] + [InlineData(ServiceType.AmazonBedrock, Skip = "This can fail. Anthropic does not support this feature yet.")] + public async Task ChatStreamingOnlyUserMessagesAsync(ServiceType serviceType) + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("I'm very thirsty."); + chatHistory.AddUserMessage("Could you give me a glass of..."); + + var sut = this.GetChatService(serviceType); + + // Act + var responses = await sut.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + string[] words = ["water", "juice", "milk", "soda", "tea", "coffee", "beer", "wine"]; + Assert.NotEmpty(responses); + var message = string.Concat(responses.Select(c => c.Content)); + this.Output.WriteLine(message); + Assert.Contains(words, word => message.Contains(word, StringComparison.OrdinalIgnoreCase)); + } } From 7b35322afc00863272470e3f58eaa3cd29da41c6 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Tue, 9 Jul 2024 19:20:09 +0200 Subject: [PATCH 08/19] Replaced Claude with Anthropic --- .../AnthropicPromptExecutionSettings.cs | 2 +- .../Core/AuthorRoleConverter.cs | 2 +- .../Core/Models/AnthropicRequest.cs | 16 ++++++++-------- .../Core/Models/Message/AnthropicContent.cs | 2 +- .../AnthropicKernelBuilderExtensions.cs | 2 +- .../AnthropicServiceCollectionExtensions.cs | 6 +++--- .../Models/AnthropicFinishReason.cs | 6 +++--- .../Models/AnthropicMetadata.cs | 2 +- .../Models/AnthropicUsage.cs | 2 +- .../Services/AnthropicChatCompletionService.cs | 4 ++-- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs index 4e0b711df534..e1af01ef5865 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs @@ -10,7 +10,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// -/// Represents the settings for executing a prompt with the Claude models. +/// Represents the settings for executing a prompt with the Anthropic models. /// [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public sealed class AnthropicPromptExecutionSettings : PromptExecutionSettings diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AuthorRoleConverter.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AuthorRoleConverter.cs index eb4369533bdd..d0f5d51f6a76 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AuthorRoleConverter.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AuthorRoleConverter.cs @@ -42,7 +42,7 @@ public override void Write(Utf8JsonWriter writer, AuthorRole value, JsonSerializ } else { - throw new JsonException($"Claude API doesn't support author role: {value}"); + throw new JsonException($"Anthropic API doesn't support author role: {value}"); } } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 844c32213650..165b9acc14fe 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -34,7 +34,7 @@ internal sealed class AnthropicRequest public int MaxTokens { get; set; } /// - /// A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or persona. + /// A system prompt is a way of providing context and instructions to Anthropic, such as specifying a particular goal or persona. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("system")] @@ -84,7 +84,7 @@ public void AddChatMessage(ChatMessageContent message) Verify.NotNull(this.Messages); Verify.NotNull(message); - this.Messages.Add(CreateClaudeMessageFromChatMessage(message)); + this.Messages.Add(CreateAnthropicMessageFromChatMessage(message)); } /// @@ -105,14 +105,14 @@ internal static AnthropicRequest FromChatHistoryAndExecutionSettings( } private static void AddMessages(IEnumerable chatHistory, AnthropicRequest request) - => request.Messages = chatHistory.Select(CreateClaudeMessageFromChatMessage).ToList(); + => request.Messages = chatHistory.Select(CreateAnthropicMessageFromChatMessage).ToList(); - private static Message CreateClaudeMessageFromChatMessage(ChatMessageContent message) + private static Message CreateAnthropicMessageFromChatMessage(ChatMessageContent message) { return new Message { Role = message.Role, - Contents = CreateClaudeMessages(message) + Contents = CreateAnthropicMessages(message) }; } @@ -136,12 +136,12 @@ private static AnthropicRequest CreateRequest(ChatHistory chatHistory, Anthropic return request; } - private static List CreateClaudeMessages(ChatMessageContent content) + private static List CreateAnthropicMessages(ChatMessageContent content) { - return content.Items.Select(GetClaudeMessageFromKernelContent).ToList(); + return content.Items.Select(GetAnthropicMessageFromKernelContent).ToList(); } - private static AnthropicContent GetClaudeMessageFromKernelContent(KernelContent content) => content switch + private static AnthropicContent GetAnthropicMessageFromKernelContent(KernelContent content) => content switch { TextContent textContent => new AnthropicTextContent { Text = textContent.Text ?? string.Empty }, ImageContent imageContent => CreateAnthropicImageContent(imageContent), diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index 2cfcbe9996fb..ec9df706bb4c 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -3,7 +3,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// -/// Represents the request/response content of Claude. +/// Represents the request/response content of Anthropic. /// [HackyJsonDerived(typeof(AnthropicTextContent), typeDiscriminator: "text")] [HackyJsonDerived(typeof(AnthropicDeltaTextContent), typeDiscriminator: "text_delta")] diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index a4085a461d94..43b52a626957 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -21,7 +21,7 @@ public static class AnthropicKernelBuilderExtensions /// /// The kernel builder. /// The model for chat completion. - /// The API key for authentication Claude API. + /// The API key for authentication Anthropic API. /// Optional options for the anthropic client /// The optional service ID. /// The optional custom HttpClient. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 84da12cc4a0e..9f98b49a1646 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -19,9 +19,9 @@ public static class AnthropicServiceCollectionExtensions /// /// Add Anthropic Chat Completion and Text Generation services to the specified service collection. /// - /// The service collection to add the Claude Text Generation service to. + /// The service collection to add the Anthropic Text Generation service to. /// The model for chat completion. - /// The API key for authentication Claude API. + /// The API key for authentication Anthropic API. /// Optional options for the anthropic client /// Optional service ID. /// The updated service collection. @@ -49,7 +49,7 @@ public static IServiceCollection AddAnthropicChatCompletion( /// /// Add Anthropic Chat Completion and Text Generation services to the specified service collection. /// - /// The service collection to add the Claude Text Generation service to. + /// The service collection to add the Anthropic Text Generation service to. /// The model for chat completion. /// Endpoint for the chat completion model /// A custom request handler to be used for sending HTTP requests diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs index 34c911b0ea78..ae1313d95663 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// /// Represents a Anthropic Finish Reason. /// -[JsonConverter(typeof(ClaudeFinishReasonConverter))] +[JsonConverter(typeof(AnthropicFinishReasonConverter))] public readonly struct AnthropicFinishReason : IEquatable { /// @@ -39,7 +39,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; public string Label { get; } /// - /// Represents a Claude Finish Reason. + /// Represents a Anthropic Finish Reason. /// [JsonConstructor] public AnthropicFinishReason(string label) @@ -82,7 +82,7 @@ public override int GetHashCode() public override string ToString() => this.Label ?? string.Empty; } -internal sealed class ClaudeFinishReasonConverter : JsonConverter +internal sealed class AnthropicFinishReasonConverter : JsonConverter { public override AnthropicFinishReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new(reader.GetString()!); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs index d2a25ccf5ddf..3eeec3808cfe 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// -/// Represents the metadata associated with a Claude response. +/// Represents the metadata associated with a Anthropic response. /// public sealed class AnthropicMetadata : ReadOnlyDictionary { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs index b05356684b03..994f5dae5c56 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs @@ -10,7 +10,7 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// Under the hood, the API transforms requests into a format suitable for the model. /// The model's output then goes through a parsing stage before becoming an API response. /// As a result, the token counts in usage will not match one-to-one with the exact visible content of an API request or response.
-/// For example, OutputTokens will be non-zero, even for an empty string response from Claude. +/// For example, OutputTokens will be non-zero, even for an empty string response from Anthropic. ///
public sealed class AnthropicUsage { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index d25127a80a32..6b3890a4aefb 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -27,7 +27,7 @@ public sealed class AnthropicChatCompletionService : IChatCompletionService /// The model for the chat completion service. /// The API key for authentication. /// Optional options for the anthropic client - /// Optional HTTP client to be used for communication with the Claude API. + /// Optional HTTP client to be used for communication with the Anthropic API. /// Optional logger factory to be used for logging. public AnthropicChatCompletionService( string modelId, @@ -57,7 +57,7 @@ public AnthropicChatCompletionService( /// Endpoint for the chat completion model /// A custom request handler to be used for sending HTTP requests /// Options for the anthropic client - /// Optional HTTP client to be used for communication with the Claude API. + /// Optional HTTP client to be used for communication with the Anthropic API. /// Optional logger factory to be used for logging. public AnthropicChatCompletionService( string modelId, From 533f4db14e280238b170ce94ff460344ad744b58 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Tue, 9 Jul 2024 19:53:05 +0200 Subject: [PATCH 09/19] Addressed feedback --- .../Core/AnthropicChatGenerationTests.cs | 19 ++++----- .../Core/AnthropicRequestTests.cs | 9 +++- ...thropicServiceCollectionExtensionsTests.cs | 7 +--- .../AnthropicClientOptions.cs | 14 +++---- .../Core/AnthropicClient.cs | 30 ++++++-------- .../Core/Models/AnthropicRequest.cs | 6 ++- .../Core/Models/AnthropicResponse.cs | 2 +- .../Message/AnthropicDeltaJsonContent.cs | 2 +- .../Message/AnthropicDeltaTextContent.cs | 2 +- .../Models/Message/AnthropicImageContent.cs | 8 ++-- .../Models/Message/AnthropicTextContent.cs | 2 +- ...ator.cs => PolymorphicJsonConverterOfT.cs} | 2 +- .../AnthropicKernelBuilderExtensions.cs | 5 --- .../AnthropicServiceCollectionExtensions.cs | 6 --- .../Models/AnthropicChatMessageContent.cs | 31 +++----------- .../AnthropicChatCompletionService.cs | 4 -- .../Connectors/Anthropic/TestBase.cs | 41 ++++++++++++++----- 17 files changed, 85 insertions(+), 105 deletions(-) rename dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/{TypeJsonDyscriminator.cs => PolymorphicJsonConverterOfT.cs} (98%) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index 9de777245e3f..9d4cd20feb1f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -103,7 +103,7 @@ public async Task ShouldReturnValidAnthropicMetadataAsync() Assert.NotNull(textContent); var metadata = textContent.Metadata as AnthropicMetadata; Assert.NotNull(metadata); - Assert.Equal(response.FinishReason, metadata.FinishReason); + Assert.Equal(response.StopReason, metadata.FinishReason); Assert.Equal(response.Id, metadata.MessageId); Assert.Equal(response.StopSequence, metadata.StopSequence); Assert.Equal(response.Usage.InputTokens, metadata.InputTokenCount); @@ -127,7 +127,7 @@ public async Task ShouldReturnValidDictionaryMetadataAsync() Assert.NotNull(textContent); var metadata = textContent.Metadata; Assert.NotNull(metadata); - Assert.Equal(response.FinishReason, metadata[nameof(AnthropicMetadata.FinishReason)]); + Assert.Equal(response.StopReason, metadata[nameof(AnthropicMetadata.FinishReason)]); Assert.Equal(response.Id, metadata[nameof(AnthropicMetadata.MessageId)]); Assert.Equal(response.StopSequence, metadata[nameof(AnthropicMetadata.StopSequence)]); Assert.Equal(response.Usage.InputTokens, metadata[nameof(AnthropicMetadata.InputTokenCount)]); @@ -231,8 +231,7 @@ public async Task ShouldPassVersionToRequestBodyIfCustomHandlerUsedAsync() httpClient: this._httpClient, modelId: "fake-model", options: new AnthropicClientOptions(), - endpoint: new Uri("https://fake-uri.com"), - requestHandler: _ => ValueTask.CompletedTask); + endpoint: new Uri("https://fake-uri.com")); var chatHistory = CreateSampleChatHistory(); // Act @@ -374,17 +373,13 @@ public async Task ItCreatesRequestWithCustomUriAndCustomHeadersAsync() // Arrange Uri uri = new("https://fake-uri.com"); (string headerName, string headerValue) = ("custom-header", "custom-value"); - ValueTask RequestHandler(HttpRequestMessage arg) - { - arg.Headers.Add(headerName, headerValue); - return ValueTask.CompletedTask; - } + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add(headerName, headerValue); var client = new AnthropicClient( - httpClient: this._httpClient, + httpClient: httpClient, modelId: "fake-model", options: new AnthropicClientOptions(), - endpoint: uri, - requestHandler: RequestHandler); + endpoint: uri); var chatHistory = CreateSampleChatHistory(); diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs index 4bcb7aacaa0f..274361862618 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; @@ -202,7 +203,13 @@ public void AddChatMessageToRequestItAddsChatMessage() // Arrange ChatHistory chat = []; var request = AnthropicRequest.FromChatHistoryAndExecutionSettings(chat, new AnthropicPromptExecutionSettings { ModelId = "model-id", MaxTokens = 128 }); - var message = new AnthropicChatMessageContent(AuthorRole.User, [new TextContent("user-message")], "model-id"); + var message = new AnthropicChatMessageContent + { + Role = AuthorRole.User, + Items = [new TextContent("user-message")], + ModelId = "model-id", + Encoding = Encoding.UTF8 + }; // Act request.AddChatMessage(message); diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs index 80271f97f94d..b06c47265aa4 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; @@ -55,8 +54,7 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInKern // Act kernelBuilder.AddAnthropicChatCompletion( - "modelId", new Uri("https://example.com"), - _ => ValueTask.CompletedTask, new AnthropicClientOptions()); + "modelId", new Uri("https://example.com"), new AnthropicClientOptions()); var kernel = kernelBuilder.Build(); // Assert @@ -73,8 +71,7 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInServ // Act services.AddAnthropicChatCompletion( - "modelId", new Uri("https://example.com"), - _ => ValueTask.CompletedTask, new AnthropicClientOptions()); + "modelId", new Uri("https://example.com"), new AnthropicClientOptions()); var serviceProvider = services.BuildServiceProvider(); // Assert diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs index 6d469ff1e3c5..a8acb339f462 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs @@ -26,10 +26,10 @@ public sealed class AnthropicClientOptions : ClientOptions public enum ServiceVersion { /// Service version "2023-01-01". - V2023_01_01 = 1, + V2023_01_01 = 0, /// Service version "2023-06-01". - V2023_06_01 = 2, + V2023_06_01 = 1, } /// @@ -46,7 +46,7 @@ public AnthropicClientOptions(ServiceVersion version = LatestVersion) { ServiceVersion.V2023_01_01 => "2023-01-01", ServiceVersion.V2023_06_01 => "2023-06-01", - _ => throw new NotSupportedException("Unsupported service version") + _ => throw new ArgumentOutOfRangeException(version.ToString()) }; } } @@ -62,7 +62,7 @@ public sealed class VertexAIAnthropicClientOptions : ClientOptions public enum ServiceVersion { /// Service version "vertex-2023-10-16". - V2023_10_16 = 1, + V2023_10_16 = 0, } /// @@ -78,7 +78,7 @@ public VertexAIAnthropicClientOptions(ServiceVersion version = LatestVersion) this.Version = version switch { ServiceVersion.V2023_10_16 => "vertex-2023-10-16", - _ => throw new NotSupportedException("Unsupported service version") + _ => throw new ArgumentOutOfRangeException(version.ToString()) }; } } @@ -94,7 +94,7 @@ public sealed class AmazonBedrockAnthropicClientOptions : ClientOptions public enum ServiceVersion { /// Service version "bedrock-2023-05-31". - V2023_05_31 = 1, + V2023_05_31 = 0, } /// @@ -110,7 +110,7 @@ public AmazonBedrockAnthropicClientOptions(ServiceVersion version = LatestVersio this.Version = version switch { ServiceVersion.V2023_05_31 => "bedrock-2023-05-31", - _ => throw new NotSupportedException("Unsupported service version") + _ => throw new ArgumentOutOfRangeException(version.ToString()) }; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index 1fa1e105bbef..62db3a30d759 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -39,7 +39,6 @@ internal sealed class AnthropicClient private readonly string _modelId; private readonly string? _apiKey; private readonly Uri _endpoint; - private readonly Func? _customRequestHandler; private readonly ClientOptions _options; private static readonly string s_namespace = typeof(AnthropicChatCompletionService).Namespace!; @@ -109,28 +108,24 @@ public AnthropicClient( /// HttpClient instance used to send HTTP requests /// Id of the model supporting chat completion /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests /// Options for the client /// Logger instance used for logging (optional) public AnthropicClient( HttpClient httpClient, string modelId, Uri endpoint, - Func requestHandler, ClientOptions options, ILogger? logger = null) { Verify.NotNull(httpClient); Verify.NotNullOrWhiteSpace(modelId); Verify.NotNull(endpoint); - Verify.NotNull(requestHandler); Verify.NotNull(options); this._httpClient = httpClient; this._logger = logger ?? NullLogger.Instance; this._modelId = modelId; this._endpoint = endpoint; - this._customRequestHandler = requestHandler; this._options = options; } @@ -211,19 +206,22 @@ private AnthropicChatMessageContent GetChatMessageContentFromAnthropicContent(An throw new NotSupportedException($"Content type {content.GetType()} is not supported yet."); } - return new AnthropicChatMessageContent( - role: response.Role, - items: [new TextContent(textContent.Text ?? string.Empty)], - modelId: response.ModelId ?? this._modelId, - innerContent: response, - metadata: GetResponseMetadata(response)); + return new AnthropicChatMessageContent + { + Role = response.Role, + Items = [new TextContent(textContent.Text ?? string.Empty)], + ModelId = response.ModelId ?? this._modelId, + InnerContent = response, + Metadata = GetResponseMetadata(response), + Encoding = Encoding.UTF8 + }; } private static AnthropicMetadata GetResponseMetadata(AnthropicResponse response) => new() { MessageId = response.Id, - FinishReason = response.FinishReason, + FinishReason = response.StopReason, StopSequence = response.StopSequence, InputTokenCount = response.Usage?.InputTokens ?? 0, OutputTokenCount = response.Usage?.OutputTokens ?? 0 @@ -266,7 +264,7 @@ private ChatCompletionState ValidateInputAndCreateChatCompletionState( var filteredChatHistory = new ChatHistory(chatHistory.Where(IsAssistantOrUserOrSystem)); var anthropicRequest = AnthropicRequest.FromChatHistoryAndExecutionSettings(filteredChatHistory, anthropicExecutionSettings); - if (this._customRequestHandler != null) + if (this._options is not AnthropicClientOptions) { anthropicRequest.Version = this._options.Version; } @@ -361,11 +359,7 @@ private async Task CreateHttpRequestAsync(object requestData httpRequestMessage.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); - if (this._customRequestHandler != null) - { - await this._customRequestHandler(httpRequestMessage).ConfigureAwait(false); - } - else + if (this._options is AnthropicClientOptions) { httpRequestMessage.Headers.Add("anthropic-version", this._options.Version); httpRequestMessage.Headers.Add("x-api-key", this._apiKey); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 165b9acc14fe..5f1518dfae3c 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -25,7 +25,7 @@ internal sealed class AnthropicRequest /// from the content in that message. This can be used to constrain part of the model's response. /// [JsonPropertyName("messages")] - public IList Messages { get; set; } = null!; + public IList Messages { get; } = new List(); [JsonPropertyName("model")] public string ModelId { get; set; } = null!; @@ -79,6 +79,8 @@ internal sealed class AnthropicRequest [JsonPropertyName("top_k")] public int? TopK { get; set; } + private AnthropicRequest() { } + public void AddChatMessage(ChatMessageContent message) { Verify.NotNull(this.Messages); @@ -105,7 +107,7 @@ internal static AnthropicRequest FromChatHistoryAndExecutionSettings( } private static void AddMessages(IEnumerable chatHistory, AnthropicRequest request) - => request.Messages = chatHistory.Select(CreateAnthropicMessageFromChatMessage).ToList(); + => request.Messages.AddRange(chatHistory.Select(CreateAnthropicMessageFromChatMessage)); private static Message CreateAnthropicMessageFromChatMessage(ChatMessageContent message) { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs index b5e57540a50b..517717b81e3d 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs @@ -30,7 +30,7 @@ internal sealed class AnthropicResponse public string ModelId { get; init; } = null!; [JsonPropertyName("stop_reason")] - public AnthropicFinishReason? FinishReason { get; init; } + public AnthropicFinishReason? StopReason { get; init; } [JsonPropertyName("stop_sequence")] public string? StopSequence { get; init; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs index 6be43608945c..08b1e5f7311e 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs @@ -11,5 +11,5 @@ internal sealed class AnthropicDeltaJsonContent : AnthropicContent /// [JsonRequired] [JsonPropertyName("partial_json")] - public string PartialJson { get; set; } + public string PartialJson { get; set; } = null!; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs index 70f275be92c2..94f3392c55d5 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs @@ -11,5 +11,5 @@ internal sealed class AnthropicDeltaTextContent : AnthropicContent /// [JsonRequired] [JsonPropertyName("text")] - public string Text { get; set; } + public string Text { get; set; } = null!; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs index ea38ea5e0d76..991d732288d1 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs @@ -10,7 +10,7 @@ internal sealed class AnthropicImageContent : AnthropicContent /// Only used when type is "image". The image content. ///
[JsonPropertyName("source")] - public SourceEntity Source { get; set; } + public SourceEntity Source { get; set; } = null!; internal sealed class SourceEntity { @@ -18,18 +18,18 @@ internal sealed class SourceEntity /// Currently supported only base64. ///
[JsonPropertyName("type")] - public string Type { get; set; } + public string Type { get; set; } = null!; /// /// The media type of the image. /// [JsonPropertyName("media_type")] - public string MediaType { get; set; } + public string MediaType { get; set; } = null!; /// /// The base64 encoded image data. /// [JsonPropertyName("data")] - public string Data { get; set; } + public string Data { get; set; } = null!; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs index 58256cb99e81..4e8c137adbba 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs @@ -11,5 +11,5 @@ internal sealed class AnthropicTextContent : AnthropicContent ///
[JsonRequired] [JsonPropertyName("text")] - public string Text { get; set; } + public string Text { get; set; } = null!; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterOfT.cs similarity index 98% rename from dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterOfT.cs index 09941962f855..b05b2c893c17 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterOfT.cs @@ -89,7 +89,7 @@ public PolymorphicJsonConverter(JsonSerializerOptions options) public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert; public override T Read( - ref Utf8JsonReader reader, Type objectType, JsonSerializerOptions options) + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var reader2 = reader; using var doc = JsonDocument.ParseValue(ref reader2); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index 43b52a626957..6568f4d165ea 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; @@ -54,7 +53,6 @@ public static IKernelBuilder AddAnthropicChatCompletion( /// The kernel builder. /// The model for chat completion. /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests /// Options for the anthropic client /// The optional service ID. /// The optional custom HttpClient. @@ -63,7 +61,6 @@ public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, string modelId, Uri endpoint, - Func requestHandler, ClientOptions options, string? serviceId = null, HttpClient? httpClient = null) @@ -72,13 +69,11 @@ public static IKernelBuilder AddAnthropicChatCompletion( Verify.NotNull(modelId); Verify.NotNull(endpoint); Verify.NotNull(options); - Verify.NotNull(requestHandler); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( modelId: modelId, endpoint: endpoint, - requestHandler: requestHandler, options: options, httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), loggerFactory: serviceProvider.GetService())); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 9f98b49a1646..a67001280742 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; @@ -52,7 +50,6 @@ public static IServiceCollection AddAnthropicChatCompletion( /// The service collection to add the Anthropic Text Generation service to. /// The model for chat completion. /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests /// Options for the anthropic client /// Optional service ID. /// The updated service collection. @@ -60,21 +57,18 @@ public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, string modelId, Uri endpoint, - Func requestHandler, ClientOptions options, string? serviceId = null) { Verify.NotNull(services); Verify.NotNull(modelId); Verify.NotNull(endpoint); - Verify.NotNull(requestHandler); Verify.NotNull(options); services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( modelId: modelId, endpoint: endpoint, - requestHandler: requestHandler, options: options, httpClient: HttpClientProvider.GetHttpClient(serviceProvider), loggerFactory: serviceProvider.GetService())); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs index 3e33d751fb60..4f70b5879d83 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Connectors.Anthropic; @@ -17,31 +15,12 @@ public sealed class AnthropicChatMessageContent : ChatMessageContent [JsonConstructor] internal AnthropicChatMessageContent() { } - /// - /// Initializes a new instance of the class. - /// - /// Role of the author of the message - /// Instance of with content items - /// The model ID used to generate the content - /// Inner content object reference - /// Additional metadata - internal AnthropicChatMessageContent( - AuthorRole role, - ChatMessageContentItemCollection items, - string modelId, - object? innerContent = null, - AnthropicMetadata? metadata = null) - : base( - role: role, - items: items, - modelId: modelId, - innerContent: innerContent, - encoding: Encoding.UTF8, - metadata: metadata) - { } - /// /// The metadata associated with the content. /// - public new AnthropicMetadata? Metadata => (AnthropicMetadata?)base.Metadata; + public new AnthropicMetadata? Metadata + { + get => base.Metadata as AnthropicMetadata; + init => base.Metadata = value; + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index 6b3890a4aefb..ad41bbb5b260 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -55,14 +55,12 @@ public AnthropicChatCompletionService( ///
/// The model for the chat completion service. /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests /// Options for the anthropic client /// Optional HTTP client to be used for communication with the Anthropic API. /// Optional logger factory to be used for logging. public AnthropicChatCompletionService( string modelId, Uri endpoint, - Func requestHandler, ClientOptions options, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) @@ -70,7 +68,6 @@ public AnthropicChatCompletionService( Verify.NotNullOrWhiteSpace(modelId); Verify.NotNull(endpoint); Verify.NotNull(options); - Verify.NotNull(requestHandler); this._client = new AnthropicClient( #pragma warning disable CA2000 @@ -78,7 +75,6 @@ public AnthropicChatCompletionService( #pragma warning restore CA2000 modelId: modelId, endpoint: endpoint, - requestHandler: requestHandler, options: options, logger: loggerFactory?.CreateLogger(typeof(AnthropicChatCompletionService))); this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs index 8b51dc55c2d2..531e21d6164a 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading.Tasks; +using System.Net.Http; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; @@ -9,16 +9,26 @@ namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; -public abstract class TestBase(ITestOutputHelper output) +public abstract class TestBase : IDisposable { private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) .AddUserSecrets() .AddEnvironmentVariables() .Build(); - protected ITestOutputHelper Output { get; } = output; + private readonly HttpClient _vertexHttpClient; + private readonly HttpClient _awsHttpClient; + + protected TestBase(ITestOutputHelper output) + { + this.Output = output; + this._vertexHttpClient = new HttpClient { DefaultRequestHeaders = { { "Authorization", $"Bearer {this.VertexAIGetBearerKey()}" } } }; + this._awsHttpClient = new HttpClient(); // TODO: setup aws bedrock claude + } + + protected ITestOutputHelper Output { get; } protected IChatCompletionService GetChatService(ServiceType serviceType) => serviceType switch { @@ -29,16 +39,12 @@ public abstract class TestBase(ITestOutputHelper output) modelId: this.VertexAIGetModel(), endpoint: new Uri(this.VertexAIGetEndpoint()), options: new VertexAIAnthropicClientOptions(), - requestHandler: requestMessage => - { - requestMessage.Headers.Authorization = new("Bearer", this.VertexAIGetBearerKey()); - return ValueTask.CompletedTask; - }), + httpClient: this._vertexHttpClient), ServiceType.AmazonBedrock => new AnthropicChatCompletionService( modelId: this.AmazonBedrockGetModel(), endpoint: new Uri(this.AmazonBedrockGetEndpoint()), options: new AmazonBedrockAnthropicClientOptions(), - requestHandler: _ => throw new NotImplementedException("setup later")), // TODO: setup aws bedrock claude + httpClient: this._awsHttpClient), // TODO: setup aws bedrock claude _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; @@ -56,4 +62,19 @@ public enum ServiceType private string VertexAIGetBearerKey() => this._configuration.GetSection("VertexAI:BearerKey").Get()!; private string AmazonBedrockGetModel() => this._configuration.GetSection("AmazonBedrock:Anthropic:ModelId").Get()!; private string AmazonBedrockGetEndpoint() => this._configuration.GetSection("AmazonBedrock:Anthropic:Endpoint").Get()!; + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this._vertexHttpClient.Dispose(); + this._awsHttpClient.Dispose(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } } From 6672a966a45f3eb81ae4e4d8b83ea9aa49fa153f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:07:17 +0100 Subject: [PATCH 10/19] Address PR Feedback + improvements --- dotnet/Directory.Packages.props | 2 +- .../Core/AnthropicChatGenerationTests.cs | 81 +++++++--- .../Core/AnthropicRequestTests.cs | 4 +- ...thropicServiceCollectionExtensionsTests.cs | 25 ++- .../AnthropicChatCompletionServiceTests.cs | 2 +- .../AnthropicClientOptions.cs | 39 ++++- .../Core/AnthropicClient.cs | 146 +++++++++--------- .../Core/Models/AnthropicResponse.cs | 30 ++++ .../Core/Models/Message/AnthropicContent.cs | 8 +- .../Message/AnthropicDeltaJsonContent.cs | 2 +- .../Message/AnthropicDeltaTextContent.cs | 2 +- .../Models/Message/AnthropicImageContent.cs | 8 +- .../Models/Message/AnthropicTextContent.cs | 2 +- .../Message/InternalJsonDerivedAttribute.cs | 16 ++ .../Message/JsonTypeDiscriminatorHelper.cs | 40 +++++ .../Message/PolymorphicJsonConverter.cs | 59 +++++++ .../PolymorphicJsonConverterFactory.cs | 23 +++ .../Models/Message/TypeJsonDyscriminator.cs | 121 --------------- .../AnthropicKernelBuilderExtensions.cs | 52 +------ .../AnthropicServiceCollectionExtensions.cs | 51 +----- .../Models/AnthropicMetadata.cs | 10 +- .../Models/AnthropicUsage.cs | 4 +- .../AnthropicChatCompletionService.cs | 47 +----- .../Connectors/Anthropic/TestBase.cs | 31 ++-- 24 files changed, 392 insertions(+), 413 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 24495d03f3c5..6891cbd077a0 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -34,7 +34,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index 9de777245e3f..b4467f7c9677 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; @@ -228,11 +229,13 @@ public async Task ShouldPassVersionToRequestBodyIfCustomHandlerUsedAsync() // Arrange var options = new AnthropicClientOptions(); var client = new AnthropicClient( - httpClient: this._httpClient, - modelId: "fake-model", - options: new AnthropicClientOptions(), - endpoint: new Uri("https://fake-uri.com"), - requestHandler: _ => ValueTask.CompletedTask); + options: new AnthropicClientOptions + { + ModelId = "fake-model", + Endpoint = new Uri("https://fake-uri.com") + }, + httpClient: this._httpClient); + var chatHistory = CreateSampleChatHistory(); // Act @@ -368,23 +371,17 @@ public async Task ItCreatesRequestWithJsonContentTypeAsync() Assert.Contains("application/json", this._messageHandlerStub.ContentHeaders.ContentType.ToString()); } - [Fact] - public async Task ItCreatesRequestWithCustomUriAndCustomHeadersAsync() + [Theory] + [InlineData("custom-header", "custom-value")] + public async Task ItCreatesRequestWithCustomUriAndCustomHeadersAsync(string headerName, string headerValue) { // Arrange Uri uri = new("https://fake-uri.com"); - (string headerName, string headerValue) = ("custom-header", "custom-value"); - ValueTask RequestHandler(HttpRequestMessage arg) - { - arg.Headers.Add(headerName, headerValue); - return ValueTask.CompletedTask; - } + using var httpHandler = new CustomHeadersHandler(headerName, headerValue); + using var httpClient = new HttpClient(httpHandler); var client = new AnthropicClient( - httpClient: this._httpClient, - modelId: "fake-model", - options: new AnthropicClientOptions(), - endpoint: uri, - requestHandler: RequestHandler); + httpClient: httpClient, + options: new AnthropicClientOptions { ModelId = "fake-model", Endpoint = uri }); var chatHistory = CreateSampleChatHistory(); @@ -392,9 +389,9 @@ ValueTask RequestHandler(HttpRequestMessage arg) await client.GenerateChatMessageAsync(chatHistory); // Assert - Assert.Equal(uri, this._messageHandlerStub.RequestUri); - Assert.NotNull(this._messageHandlerStub.RequestHeaders); - Assert.Equal(headerValue, this._messageHandlerStub.RequestHeaders.GetValues(headerName).SingleOrDefault()); + Assert.Equal(uri, httpHandler.RequestUri); + Assert.NotNull(httpHandler.RequestHeaders); + Assert.Equal(headerValue, httpHandler.RequestHeaders.GetValues(headerName).SingleOrDefault()); } private static ChatHistory CreateSampleChatHistory() @@ -413,10 +410,8 @@ private AnthropicClient CreateChatCompletionClient( HttpClient? httpClient = null) { return new AnthropicClient( - httpClient: httpClient ?? this._httpClient, - modelId: modelId, - options: options, - apiKey: apiKey ?? "fake-key"); + options: new AnthropicClientOptions { ModelId = modelId, ApiKey = apiKey ?? "fake-key" }, + httpClient: httpClient ?? this._httpClient); } private static T? Deserialize(string json) @@ -434,4 +429,40 @@ public void Dispose() this._httpClient.Dispose(); this._messageHandlerStub.Dispose(); } + + private sealed class CustomHeadersHandler : DelegatingHandler + { + private readonly string _headerName; + private readonly string _headerValue; + public HttpRequestHeaders? RequestHeaders { get; private set; } + + public HttpContentHeaders? ContentHeaders { get; private set; } + + public byte[]? RequestContent { get; private set; } + + public Uri? RequestUri { get; private set; } + + public HttpMethod? Method { get; private set; } + + public CustomHeadersHandler(string headerName, string headerValue) + { + this.InnerHandler = new HttpMessageHandlerStub + { + ResponseToReturn = { Content = new StringContent(File.ReadAllText(ChatTestDataFilePath)) } + }; + this._headerName = headerName; + this._headerValue = headerValue; + } + + protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + request.Headers.Add(this._headerName, this._headerValue); + this.Method = request.Method; + this.RequestUri = request.RequestUri; + this.RequestHeaders = request.Headers; + this.RequestContent = request.Content is null ? null : request.Content.ReadAsByteArrayAsync(cancellationToken).Result; + + return base.SendAsync(request, cancellationToken); + } + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs index 4bcb7aacaa0f..cb3629196b7c 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs @@ -146,8 +146,8 @@ public void FromChatHistoryImageAsImageContentItReturnsWithChatHistory() c => Assert.Equal(chatHistory[1].Content, ((AnthropicTextContent)c.Contents[0]).Text), c => { - Assert.Equal(chatHistory[2].Items.Cast().Single().MimeType, ((AnthropicImageContent)c.Contents[0]).Source.MediaType); - Assert.True(imageAsBytes.ToArray().SequenceEqual(Convert.FromBase64String(((AnthropicImageContent)c.Contents[0]).Source.Data))); + Assert.Equal(chatHistory[2].Items.Cast().Single().MimeType, ((AnthropicImageContent)c.Contents[0]).Source!.MediaType); + Assert.True(imageAsBytes.ToArray().SequenceEqual(Convert.FromBase64String(((AnthropicImageContent)c.Contents[0]).Source!.Data!))); }); } diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs index 80271f97f94d..dd356f26e70d 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; @@ -22,7 +21,12 @@ public void AnthropicChatCompletionServiceShouldBeRegisteredInKernelServices() var kernelBuilder = Kernel.CreateBuilder(); // Act - kernelBuilder.AddAnthropicChatCompletion("modelId", "apiKey"); + kernelBuilder.AddAnthropicChatCompletion(new AnthropicClientOptions + { + ModelId = "modelId", + ApiKey = "apiKey" + }); + var kernel = kernelBuilder.Build(); // Assert @@ -38,7 +42,7 @@ public void AnthropicChatCompletionServiceShouldBeRegisteredInServiceCollection( var services = new ServiceCollection(); // Act - services.AddAnthropicChatCompletion("modelId", "apiKey"); + services.AddAnthropicChatCompletion(new AnthropicClientOptions() { ModelId = "modelId", ApiKey = "apiKey" }); var serviceProvider = services.BuildServiceProvider(); // Assert @@ -54,9 +58,11 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInKern var kernelBuilder = Kernel.CreateBuilder(); // Act - kernelBuilder.AddAnthropicChatCompletion( - "modelId", new Uri("https://example.com"), - _ => ValueTask.CompletedTask, new AnthropicClientOptions()); + kernelBuilder.AddAnthropicChatCompletion(new AnthropicClientOptions + { + ModelId = "modelId", + Endpoint = new Uri("https://example.com") + }); var kernel = kernelBuilder.Build(); // Assert @@ -73,8 +79,11 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInServ // Act services.AddAnthropicChatCompletion( - "modelId", new Uri("https://example.com"), - _ => ValueTask.CompletedTask, new AnthropicClientOptions()); + new AnthropicClientOptions + { + ModelId = "modelId", + Endpoint = new Uri("https://example.com"), + }); var serviceProvider = services.BuildServiceProvider(); // Assert diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs index 94e8dca76b4f..d0b15411b36d 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs @@ -13,7 +13,7 @@ public void AttributesShouldContainModelId() { // Arrange & Act string model = "fake-model"; - var service = new AnthropicChatCompletionService(model, "key"); + var service = new AnthropicChatCompletionService(new AnthropicClientOptions { ModelId = model, ApiKey = "key" }); // Assert Assert.Equal(model, service.Attributes[AIServiceExtensions.ModelIdKey]); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs index 6d469ff1e3c5..db57f21b64d3 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs @@ -5,7 +5,6 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; #pragma warning disable CA1707 // Identifiers should not contain underscores -#pragma warning disable CA1008 // Enums should have zero value /// /// Represents the options for configuring the Anthropic client. @@ -13,6 +12,21 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; public abstract class ClientOptions { internal string Version { get; private protected init; } = null!; + + /// + /// The service identifier. + /// + public string? ServiceId { get; init; } + + /// + /// Non-default Anthropic endpoint. + /// + public Uri? Endpoint { get; init; } + + /// + /// The target model ID. + /// + public string? ModelId { get; init; } } /// @@ -22,14 +36,19 @@ public sealed class AnthropicClientOptions : ClientOptions { private const ServiceVersion LatestVersion = ServiceVersion.V2023_06_01; + /// + /// The API key for authentication. + /// + public string? ApiKey { get; init; } + /// The version of the service to use. public enum ServiceVersion { /// Service version "2023-01-01". - V2023_01_01 = 1, + V2023_01_01, /// Service version "2023-06-01". - V2023_06_01 = 2, + V2023_06_01, } /// @@ -58,11 +77,16 @@ public sealed class VertexAIAnthropicClientOptions : ClientOptions { private const ServiceVersion LatestVersion = ServiceVersion.V2023_10_16; + /// + /// The Bearer key for authentication. + /// + public string? BearerKey { get; init; } + /// The version of the service to use. public enum ServiceVersion { /// Service version "vertex-2023-10-16". - V2023_10_16 = 1, + V2023_10_16, } /// @@ -90,11 +114,16 @@ public sealed class AmazonBedrockAnthropicClientOptions : ClientOptions { private const ServiceVersion LatestVersion = ServiceVersion.V2023_05_31; + /// + /// The Bearer key for authentication. + /// + public string? BearerKey { get; init; } + /// The version of the service to use. public enum ServiceVersion { /// Service version "bedrock-2023-05-31". - V2023_05_31 = 1, + V2023_05_31, } /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index 1fa1e105bbef..3df763acf00d 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -39,8 +39,7 @@ internal sealed class AnthropicClient private readonly string _modelId; private readonly string? _apiKey; private readonly Uri _endpoint; - private readonly Func? _customRequestHandler; - private readonly ClientOptions _options; + private readonly string? _version; private static readonly string s_namespace = typeof(AnthropicChatCompletionService).Namespace!; @@ -75,63 +74,47 @@ internal sealed class AnthropicClient name: $"{s_namespace}.tokens.total", unit: "{token}", description: "Number of tokens used"); + private readonly string? _bearerKey; /// /// Represents a client for interacting with the Anthropic chat completion models. /// - /// HttpClient instance used to send HTTP requests - /// Id of the model supporting chat completion - /// Api key /// Options for the client - /// Logger instance used for logging (optional) - public AnthropicClient( - HttpClient httpClient, - string modelId, - string apiKey, - ClientOptions? options, - ILogger? logger = null) - { - Verify.NotNull(httpClient); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - this._httpClient = httpClient; - this._logger = logger ?? NullLogger.Instance; - this._modelId = modelId; - this._apiKey = apiKey; - this._options = options ?? new AnthropicClientOptions(); - this._endpoint = new Uri("https://api.anthropic.com/v1/messages"); - } - - /// - /// Represents a client for interacting with the Anthropic chat completion models. - /// /// HttpClient instance used to send HTTP requests - /// Id of the model supporting chat completion - /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests - /// Options for the client /// Logger instance used for logging (optional) - public AnthropicClient( - HttpClient httpClient, - string modelId, - Uri endpoint, - Func requestHandler, + internal AnthropicClient( ClientOptions options, + HttpClient httpClient, ILogger? logger = null) { - Verify.NotNull(httpClient); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNull(endpoint); - Verify.NotNull(requestHandler); Verify.NotNull(options); + Verify.NotNull(httpClient); + Verify.NotNullOrWhiteSpace(options.ModelId); + + if (options is AnthropicClientOptions anthropicOptions + && options.Endpoint is null + && httpClient.BaseAddress is null) + { + // If a custom endpoint is not provided, the ApiKey is required + Verify.NotNullOrWhiteSpace(anthropicOptions.ApiKey); + this._apiKey = anthropicOptions.ApiKey; + } + else if (options is VertexAIAnthropicClientOptions vertexOptions) + { + Verify.NotNullOrWhiteSpace(vertexOptions.BearerKey); + this._bearerKey = vertexOptions.BearerKey; + } + else if (options is AmazonBedrockAnthropicClientOptions amazonOptions) + { + Verify.NotNullOrWhiteSpace(amazonOptions.BearerKey); + this._bearerKey = amazonOptions.BearerKey; + } this._httpClient = httpClient; this._logger = logger ?? NullLogger.Instance; - this._modelId = modelId; - this._endpoint = endpoint; - this._customRequestHandler = requestHandler; - this._options = options; + this._modelId = options.ModelId; + this._version = options.Version; + this._endpoint = options.Endpoint ?? httpClient.BaseAddress ?? new Uri("https://api.anthropic.com/v1/messages"); } /// @@ -142,7 +125,7 @@ public AnthropicClient( /// A kernel instance. /// A cancellation token to cancel the operation. /// Returns a list of chat message contents. - public async Task> GenerateChatMessageAsync( + internal async Task> GenerateChatMessageAsync( ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, @@ -158,7 +141,11 @@ public async Task> GenerateChatMessageAsync( try { anthropicResponse = await this.SendRequestAndReturnValidResponseAsync( - this._endpoint, state.AnthropicRequest, cancellationToken).ConfigureAwait(false); + this._endpoint, + state.AnthropicRequest, + cancellationToken) + .ConfigureAwait(false); + chatResponses = this.GetChatResponseFrom(anthropicResponse); } catch (Exception ex) when (activity is not null) @@ -196,9 +183,20 @@ private void LogUsage(List chatMessageContents) metadata.OutputTokenCount, metadata.TotalTokenCount); - s_promptTokensCounter.Add(metadata.InputTokenCount); - s_completionTokensCounter.Add(metadata.OutputTokenCount); - s_totalTokensCounter.Add(metadata.TotalTokenCount); + if (metadata.InputTokenCount.HasValue) + { + s_promptTokensCounter.Add(metadata.InputTokenCount.Value); + } + + if (metadata.OutputTokenCount.HasValue) + { + s_completionTokensCounter.Add(metadata.OutputTokenCount.Value); + } + + if (metadata.TotalTokenCount.HasValue) + { + s_totalTokensCounter.Add(metadata.TotalTokenCount.Value); + } } private List GetChatMessageContentsFromResponse(AnthropicResponse response) @@ -234,22 +232,12 @@ private async Task SendRequestAndReturnValidResponseAsync( AnthropicRequest anthropicRequest, CancellationToken cancellationToken) { - using var httpRequestMessage = await this.CreateHttpRequestAsync(anthropicRequest, endpoint).ConfigureAwait(false); - string body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken) - .ConfigureAwait(false); + using var httpRequestMessage = this.CreateHttpRequest(anthropicRequest, endpoint); + var body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); var response = DeserializeResponse(body); - ValidateAnthropicResponse(response); return response; } - private static void ValidateAnthropicResponse(AnthropicResponse response) - { - if (response.Contents is null || response.Contents.Count == 0) - { - throw new KernelException("Anthropic API doesn't return any data."); - } - } - private ChatCompletionState ValidateInputAndCreateChatCompletionState( ChatHistory chatHistory, PromptExecutionSettings? executionSettings) @@ -266,10 +254,7 @@ private ChatCompletionState ValidateInputAndCreateChatCompletionState( var filteredChatHistory = new ChatHistory(chatHistory.Where(IsAssistantOrUserOrSystem)); var anthropicRequest = AnthropicRequest.FromChatHistoryAndExecutionSettings(filteredChatHistory, anthropicExecutionSettings); - if (this._customRequestHandler != null) - { - anthropicRequest.Version = this._options.Version; - } + anthropicRequest.Version = this._version; return new ChatCompletionState { @@ -290,7 +275,7 @@ static bool IsAssistantOrUserOrSystem(ChatMessageContent msg) /// A kernel instance. /// A cancellation token to cancel the operation. /// An asynchronous enumerable of streaming chat contents. - public async IAsyncEnumerable StreamGenerateChatMessageAsync( + internal async IAsyncEnumerable StreamGenerateChatMessageAsync( ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, @@ -354,23 +339,34 @@ private static T DeserializeResponse(string body) } } - private async Task CreateHttpRequestAsync(object requestData, Uri endpoint) + private HttpRequestMessage CreateHttpRequest(object requestData, Uri endpoint) { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = CreateJsonContent(requestData) }; - httpRequestMessage.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); - httpRequestMessage.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, - HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); + if (!httpRequestMessage.Headers.Contains("User-Agent")) + { + httpRequestMessage.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); + } - if (this._customRequestHandler != null) + if (!httpRequestMessage.Headers.Contains(HttpHeaderConstant.Names.SemanticKernelVersion)) { - await this._customRequestHandler(httpRequestMessage).ConfigureAwait(false); + httpRequestMessage.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); } - else + + if (!httpRequestMessage.Headers.Contains("anthropic-version")) + { + httpRequestMessage.Headers.Add("anthropic-version", this._version); + } + + if (this._apiKey is not null && !httpRequestMessage.Headers.Contains("x-api-key")) { - httpRequestMessage.Headers.Add("anthropic-version", this._options.Version); httpRequestMessage.Headers.Add("x-api-key", this._apiKey); } + if (this._bearerKey is not null && !httpRequestMessage.Headers.Contains("Authorization")) + { + httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this._bearerKey); + } + return httpRequestMessage; } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs index b5e57540a50b..f907bd0cc286 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs @@ -6,35 +6,65 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; +/// +/// Represents the response from the Anthropic API. +/// https://docs.anthropic.com/en/api/messages +/// internal sealed class AnthropicResponse { + /// + /// Unique object identifier. + /// [JsonRequired] [JsonPropertyName("id")] public string Id { get; init; } = null!; + /// + /// Object type. + /// [JsonRequired] [JsonPropertyName("type")] public string Type { get; init; } = null!; + /// + /// Conversational role of the generated message. + /// [JsonRequired] [JsonPropertyName("role")] [JsonConverter(typeof(AuthorRoleConverter))] public AuthorRole Role { get; init; } + /// + /// Content generated by the model. + /// This is an array of content blocks, each of which has a type that determines its shape. + /// [JsonRequired] [JsonPropertyName("content")] public IReadOnlyList Contents { get; init; } = null!; + /// + /// The model that handled the request. + /// [JsonRequired] [JsonPropertyName("model")] public string ModelId { get; init; } = null!; + /// + /// The reason that we stopped. + /// [JsonPropertyName("stop_reason")] public AnthropicFinishReason? FinishReason { get; init; } + /// + /// Which custom stop sequence was generated, if any. + /// This value will be a non-null string if one of your custom stop sequences was generated. + /// [JsonPropertyName("stop_sequence")] public string? StopSequence { get; init; } + /// + /// Billing and rate-limit usage. + /// [JsonRequired] [JsonPropertyName("usage")] public AnthropicUsage Usage { get; init; } = null!; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index 2cfcbe9996fb..58c52a5e5021 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -5,8 +5,8 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// /// Represents the request/response content of Claude. /// -[HackyJsonDerived(typeof(AnthropicTextContent), typeDiscriminator: "text")] -[HackyJsonDerived(typeof(AnthropicDeltaTextContent), typeDiscriminator: "text_delta")] -[HackyJsonDerived(typeof(AnthropicDeltaJsonContent), typeDiscriminator: "input_json_delta")] -[HackyJsonDerived(typeof(AnthropicImageContent), typeDiscriminator: "image")] +[InternalJsonDerived(typeof(AnthropicTextContent), typeDiscriminator: "text")] +[InternalJsonDerived(typeof(AnthropicDeltaTextContent), typeDiscriminator: "text_delta")] +[InternalJsonDerived(typeof(AnthropicDeltaJsonContent), typeDiscriminator: "input_json_delta")] +[InternalJsonDerived(typeof(AnthropicImageContent), typeDiscriminator: "image")] internal abstract class AnthropicContent; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs index 6be43608945c..d31e7b183c38 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs @@ -11,5 +11,5 @@ internal sealed class AnthropicDeltaJsonContent : AnthropicContent /// [JsonRequired] [JsonPropertyName("partial_json")] - public string PartialJson { get; set; } + public string? PartialJson { get; set; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs index 70f275be92c2..701e43d73433 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs @@ -11,5 +11,5 @@ internal sealed class AnthropicDeltaTextContent : AnthropicContent /// [JsonRequired] [JsonPropertyName("text")] - public string Text { get; set; } + public string? Text { get; set; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs index ea38ea5e0d76..8953b1876a97 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs @@ -10,7 +10,7 @@ internal sealed class AnthropicImageContent : AnthropicContent /// Only used when type is "image". The image content. /// [JsonPropertyName("source")] - public SourceEntity Source { get; set; } + public SourceEntity? Source { get; set; } internal sealed class SourceEntity { @@ -18,18 +18,18 @@ internal sealed class SourceEntity /// Currently supported only base64. /// [JsonPropertyName("type")] - public string Type { get; set; } + public string? Type { get; set; } /// /// The media type of the image. /// [JsonPropertyName("media_type")] - public string MediaType { get; set; } + public string? MediaType { get; set; } /// /// The base64 encoded image data. /// [JsonPropertyName("data")] - public string Data { get; set; } + public string? Data { get; set; } } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs index 58256cb99e81..9000100161c2 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs @@ -11,5 +11,5 @@ internal sealed class AnthropicTextContent : AnthropicContent /// [JsonRequired] [JsonPropertyName("text")] - public string Text { get; set; } + public string? Text { get; set; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs new file mode 100644 index 000000000000..8a915a5394ed --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +/// +/// Same as but used to avoid NotSupportedExceptions when using the former. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] +internal sealed class InternalJsonDerivedAttribute(Type subtype, string typeDiscriminator) : Attribute +{ + public Type Subtype { get; internal set; } = subtype; + public string TypeDiscriminator { get; internal set; } = typeDiscriminator; +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs new file mode 100644 index 000000000000..e42e49f7716a --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +// Temporary solution from https://github.com/dotnet/runtime/issues/72604 +// TODO: Remove this once we move to .NET 9 + +internal static class JsonTypeDiscriminatorHelper +{ + internal static IJsonTypeInfoResolver TypeInfoResolver { get; } = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + var propertyNamingPolicy = typeInfo.Options.PropertyNamingPolicy; + + // Temporary hack to ensure subclasses of abstract classes will always include the type field + if (typeInfo.Type.BaseType is { IsAbstract: true } && + typeInfo.Type.BaseType.GetCustomAttributes().Any()) + { + var discriminatorPropertyName = propertyNamingPolicy?.ConvertName("type") ?? "type"; + if (typeInfo.Properties.All(p => p.Name != discriminatorPropertyName)) + { + var discriminatorValue = typeInfo.Type.BaseType + .GetCustomAttributes() + .First(attr => attr.Subtype == typeInfo.Type).TypeDiscriminator; + var propInfo = typeInfo.CreateJsonPropertyInfo(typeof(string), discriminatorPropertyName); + propInfo.Get = _ => discriminatorValue; + typeInfo.Properties.Insert(0, propInfo); + } + } + }, + }, + }; +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs new file mode 100644 index 000000000000..36696e554b29 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +/// +/// A temporary hack to support deserializing JSON payloads that use polymorphism but don't specify type as the first field. +/// Modified from https://github.com/dotnet/runtime/issues/72604#issuecomment-1440708052. +/// +internal sealed class PolymorphicJsonConverter : JsonConverter +{ + private readonly string _discriminatorPropName; + private readonly Dictionary _discriminatorToSubtype = []; + + public PolymorphicJsonConverter(JsonSerializerOptions options) + { + this._discriminatorPropName = options.PropertyNamingPolicy?.ConvertName("type") ?? "type"; + foreach (var subtype in typeof(T).GetCustomAttributes()) + { + this._discriminatorToSubtype.Add(subtype.TypeDiscriminator, subtype.Subtype); + } + } + + public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert; + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var reader2 = reader; + using var doc = JsonDocument.ParseValue(ref reader2); + + var root = doc.RootElement; + var typeField = root.GetProperty(this._discriminatorPropName); + + if (typeField.GetString() is not { } typeName) + { + throw new JsonException( + $"Could not find string property {this._discriminatorPropName} " + + $"when trying to deserialize {typeof(T).Name}"); + } + + if (!this._discriminatorToSubtype.TryGetValue(typeName, out var type)) + { + throw new JsonException($"Unknown type: {typeName}"); + } + + return (T)JsonSerializer.Deserialize(ref reader, type, options)!; + } + + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + var type = value!.GetType(); + JsonSerializer.Serialize(writer, value, type, options); + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs new file mode 100644 index 000000000000..199c55d18585 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; + +internal sealed class PolymorphicJsonConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAbstract && typeToConvert.GetCustomAttributes().Any(); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter?)Activator.CreateInstance( + typeof(PolymorphicJsonConverter<>).MakeGenericType(typeToConvert), options); + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs deleted file mode 100644 index 09941962f855..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/TypeJsonDyscriminator.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -// Temporary solution from https://github.com/dotnet/runtime/issues/72604 -// TODO: Remove this once we move to .NET 9 - -internal static class JsonTypeDiscriminatorHelper -{ - internal static IJsonTypeInfoResolver TypeInfoResolver { get; } = new DefaultJsonTypeInfoResolver - { - Modifiers = - { - static typeInfo => - { - var propertyNamingPolicy = typeInfo.Options.PropertyNamingPolicy; - - // Temporary hack to ensure subclasses of abstract classes will always include the type field - if (typeInfo.Type.BaseType is { IsAbstract: true } && - typeInfo.Type.BaseType.GetCustomAttributes().Any()) - { - var discriminatorPropertyName = propertyNamingPolicy?.ConvertName("type") ?? "type"; - if (typeInfo.Properties.All(p => p.Name != discriminatorPropertyName)) - { - var discriminatorValue = typeInfo.Type.BaseType - .GetCustomAttributes() - .First(attr => attr.Subtype == typeInfo.Type).TypeDiscriminator; - var propInfo = typeInfo.CreateJsonPropertyInfo(typeof(string), discriminatorPropertyName); - propInfo.Get = _ => discriminatorValue; - typeInfo.Properties.Insert(0, propInfo); - } - } - }, - }, - }; -} - -/// -/// Same as but used for the hack below. Necessary because using the built-in -/// attribute will lead to NotSupportedExceptions. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] -internal sealed class HackyJsonDerivedAttribute(Type subtype, string typeDiscriminator) : Attribute -{ - public Type Subtype { get; internal set; } = subtype; - public string TypeDiscriminator { get; internal set; } = typeDiscriminator; -} - -internal sealed class PolymorphicJsonConverterFactory : JsonConverterFactory -{ - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert.IsAbstract && typeToConvert.GetCustomAttributes().Any(); - } - - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - return (JsonConverter?)Activator.CreateInstance( - typeof(PolymorphicJsonConverter<>).MakeGenericType(typeToConvert), options); - } -} - -/// -/// A temporary hack to support deserializing JSON payloads that use polymorphism but don't specify type as the first field. -/// Modified from https://github.com/dotnet/runtime/issues/72604#issuecomment-1440708052. -/// -internal sealed class PolymorphicJsonConverter : JsonConverter -{ - private readonly string _discriminatorPropName; - private readonly Dictionary _discriminatorToSubtype = []; - - public PolymorphicJsonConverter(JsonSerializerOptions options) - { - this._discriminatorPropName = options.PropertyNamingPolicy?.ConvertName("type") ?? "type"; - foreach (var subtype in typeof(T).GetCustomAttributes()) - { - this._discriminatorToSubtype.Add(subtype.TypeDiscriminator, subtype.Subtype); - } - } - - public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert; - - public override T Read( - ref Utf8JsonReader reader, Type objectType, JsonSerializerOptions options) - { - var reader2 = reader; - using var doc = JsonDocument.ParseValue(ref reader2); - - var root = doc.RootElement; - var typeField = root.GetProperty(this._discriminatorPropName); - - if (typeField.GetString() is not { } typeName) - { - throw new JsonException( - $"Could not find string property {this._discriminatorPropName} " + - $"when trying to deserialize {typeof(T).Name}"); - } - - if (!this._discriminatorToSubtype.TryGetValue(typeName, out var type)) - { - throw new JsonException($"Unknown type: {typeName}"); - } - - return (T)JsonSerializer.Deserialize(ref reader, type, options)!; - } - - public override void Write( - Utf8JsonWriter writer, T? value, JsonSerializerOptions options) - { - var type = value!.GetType(); - JsonSerializer.Serialize(writer, value, type, options); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index a4085a461d94..8b67ece1f7f2 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; @@ -20,68 +18,22 @@ public static class AnthropicKernelBuilderExtensions /// Add Anthropic Chat Completion and Text Generation services to the kernel builder. /// /// The kernel builder. - /// The model for chat completion. - /// The API key for authentication Claude API. /// Optional options for the anthropic client - /// The optional service ID. /// The optional custom HttpClient. /// The updated kernel builder. public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, - string modelId, - string apiKey, - ClientOptions? options = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNull(modelId); - Verify.NotNull(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AnthropicChatCompletionService( - modelId: modelId, - apiKey: apiKey, - options: options, - httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - loggerFactory: serviceProvider.GetService())); - return builder; - } - - /// - /// Add Anthropic Chat Completion and Text Generation services to the kernel builder. - /// - /// The kernel builder. - /// The model for chat completion. - /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests - /// Options for the anthropic client - /// The optional service ID. - /// The optional custom HttpClient. - /// The updated kernel builder. - public static IKernelBuilder AddAnthropicChatCompletion( - this IKernelBuilder builder, - string modelId, - Uri endpoint, - Func requestHandler, ClientOptions options, - string? serviceId = null, HttpClient? httpClient = null) { Verify.NotNull(builder); - Verify.NotNull(modelId); - Verify.NotNull(endpoint); - Verify.NotNull(options); - Verify.NotNull(requestHandler); - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + builder.Services.AddKeyedSingleton(options.ServiceId, (serviceProvider, _) => new AnthropicChatCompletionService( - modelId: modelId, - endpoint: endpoint, - requestHandler: requestHandler, options: options, httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), loggerFactory: serviceProvider.GetService())); + return builder; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 84da12cc4a0e..84e9b447664b 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -1,8 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; @@ -20,64 +17,20 @@ public static class AnthropicServiceCollectionExtensions /// Add Anthropic Chat Completion and Text Generation services to the specified service collection. ///
/// The service collection to add the Claude Text Generation service to. - /// The model for chat completion. - /// The API key for authentication Claude API. /// Optional options for the anthropic client - /// Optional service ID. /// The updated service collection. public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, - string modelId, - string apiKey, - ClientOptions? options = null, - string? serviceId = null) + ClientOptions options) { Verify.NotNull(services); - Verify.NotNull(modelId); - Verify.NotNull(apiKey); - services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + services.AddKeyedSingleton(options.ServiceId, (serviceProvider, _) => new AnthropicChatCompletionService( - modelId: modelId, - apiKey: apiKey, options: options, httpClient: HttpClientProvider.GetHttpClient(serviceProvider), loggerFactory: serviceProvider.GetService())); - return services; - } - - /// - /// Add Anthropic Chat Completion and Text Generation services to the specified service collection. - /// - /// The service collection to add the Claude Text Generation service to. - /// The model for chat completion. - /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests - /// Options for the anthropic client - /// Optional service ID. - /// The updated service collection. - public static IServiceCollection AddAnthropicChatCompletion( - this IServiceCollection services, - string modelId, - Uri endpoint, - Func requestHandler, - ClientOptions options, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNull(modelId); - Verify.NotNull(endpoint); - Verify.NotNull(requestHandler); - Verify.NotNull(options); - services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AnthropicChatCompletionService( - modelId: modelId, - endpoint: endpoint, - requestHandler: requestHandler, - options: options, - httpClient: HttpClientProvider.GetHttpClient(serviceProvider), - loggerFactory: serviceProvider.GetService())); return services; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs index d2a25ccf5ddf..76a92d0f78c7 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs @@ -46,18 +46,18 @@ public string? StopSequence /// /// The number of input tokens which were used. /// - public int InputTokenCount + public int? InputTokenCount { - get => (this.GetValueFromDictionary(nameof(this.InputTokenCount)) as int?) ?? 0; + get => this.GetValueFromDictionary(nameof(this.InputTokenCount)) as int?; internal init => this.SetValueInDictionary(value, nameof(this.InputTokenCount)); } /// /// The number of output tokens which were used. /// - public int OutputTokenCount + public int? OutputTokenCount { - get => (this.GetValueFromDictionary(nameof(this.OutputTokenCount)) as int?) ?? 0; + get => this.GetValueFromDictionary(nameof(this.OutputTokenCount)) as int?; internal init => this.SetValueInDictionary(value, nameof(this.OutputTokenCount)); } @@ -65,7 +65,7 @@ public int OutputTokenCount /// Represents the total count of tokens in the Anthropic response, /// which is calculated by summing the input token count and the output token count. ///
- public int TotalTokenCount => this.InputTokenCount + this.OutputTokenCount; + public int? TotalTokenCount => this.InputTokenCount + this.OutputTokenCount; /// /// Converts a dictionary to a object. diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs index b05356684b03..4082cc7421ee 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs @@ -19,12 +19,12 @@ public sealed class AnthropicUsage /// [JsonRequired] [JsonPropertyName("input_tokens")] - public int InputTokens { get; init; } + public int? InputTokens { get; init; } /// /// The number of output tokens which were used /// [JsonRequired] [JsonPropertyName("output_tokens")] - public int OutputTokens { get; init; } + public int? OutputTokens { get; init; } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index d25127a80a32..77bddbfecf3f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; @@ -24,64 +23,22 @@ public sealed class AnthropicChatCompletionService : IChatCompletionService /// /// Initializes a new instance of the class. /// - /// The model for the chat completion service. - /// The API key for authentication. - /// Optional options for the anthropic client - /// Optional HTTP client to be used for communication with the Claude API. - /// Optional logger factory to be used for logging. - public AnthropicChatCompletionService( - string modelId, - string apiKey, - ClientOptions? options = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - this._client = new AnthropicClient( -#pragma warning disable CA2000 - httpClient: HttpClientProvider.GetHttpClient(httpClient), -#pragma warning restore CA2000 - modelId: modelId, - apiKey: apiKey, - options: options, - logger: loggerFactory?.CreateLogger(typeof(AnthropicChatCompletionService))); - this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Initializes a new instance of the class. - /// - /// The model for the chat completion service. - /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests /// Options for the anthropic client /// Optional HTTP client to be used for communication with the Claude API. /// Optional logger factory to be used for logging. public AnthropicChatCompletionService( - string modelId, - Uri endpoint, - Func requestHandler, ClientOptions options, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNull(endpoint); Verify.NotNull(options); - Verify.NotNull(requestHandler); + Verify.NotNullOrWhiteSpace(options.ModelId); this._client = new AnthropicClient( -#pragma warning disable CA2000 httpClient: HttpClientProvider.GetHttpClient(httpClient), -#pragma warning restore CA2000 - modelId: modelId, - endpoint: endpoint, - requestHandler: requestHandler, options: options, logger: loggerFactory?.CreateLogger(typeof(AnthropicChatCompletionService))); - this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); + this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, options.ModelId); } /// diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs index 8b51dc55c2d2..5610a6ab773f 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; @@ -23,22 +22,27 @@ public abstract class TestBase(ITestOutputHelper output) protected IChatCompletionService GetChatService(ServiceType serviceType) => serviceType switch { ServiceType.Anthropic => new AnthropicChatCompletionService( - modelId: this.AnthropicGetModel(), - apiKey: this.AnthropicGetApiKey()), + new AnthropicClientOptions + { + ModelId = this.AnthropicGetModel(), + ApiKey = this.AnthropicGetApiKey() + }), + ServiceType.VertexAI => new AnthropicChatCompletionService( - modelId: this.VertexAIGetModel(), - endpoint: new Uri(this.VertexAIGetEndpoint()), - options: new VertexAIAnthropicClientOptions(), - requestHandler: requestMessage => + new VertexAIAnthropicClientOptions { - requestMessage.Headers.Authorization = new("Bearer", this.VertexAIGetBearerKey()); - return ValueTask.CompletedTask; + ModelId = this.VertexAIGetModel(), + Endpoint = new Uri(this.VertexAIGetEndpoint()), + BearerKey = this.VertexAIGetBearerKey() }), + ServiceType.AmazonBedrock => new AnthropicChatCompletionService( - modelId: this.AmazonBedrockGetModel(), - endpoint: new Uri(this.AmazonBedrockGetEndpoint()), - options: new AmazonBedrockAnthropicClientOptions(), - requestHandler: _ => throw new NotImplementedException("setup later")), // TODO: setup aws bedrock claude + new AmazonBedrockAnthropicClientOptions + { + ModelId = this.AmazonBedrockGetModel(), + Endpoint = new Uri(this.AmazonBedrockGetEndpoint()), + BearerKey = this.AmazonBedrockGetBearerKey() // TODO: setup aws bedrock claude + }), _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; @@ -54,6 +58,7 @@ public enum ServiceType private string VertexAIGetModel() => this._configuration.GetSection("VertexAI:Anthropic:ModelId").Get()!; private string VertexAIGetEndpoint() => this._configuration.GetSection("VertexAI:Anthropic:Endpoint").Get()!; private string VertexAIGetBearerKey() => this._configuration.GetSection("VertexAI:BearerKey").Get()!; + private string AmazonBedrockGetBearerKey() => this._configuration.GetSection("AmazonBedrock:Anthropic:BearerKey").Get()!; private string AmazonBedrockGetModel() => this._configuration.GetSection("AmazonBedrock:Anthropic:ModelId").Get()!; private string AmazonBedrockGetEndpoint() => this._configuration.GetSection("AmazonBedrock:Anthropic:Endpoint").Get()!; } From eade722cf3345cf72d80bcca2f94ac00a6cc51c8 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Tue, 23 Jul 2024 21:42:02 +0200 Subject: [PATCH 11/19] Fixes --- .../Core/AnthropicChatGenerationTests.cs | 4 ++-- .../Core/AnthropicClient.cs | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index b4467f7c9677..91de91bf69c8 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -104,7 +104,7 @@ public async Task ShouldReturnValidAnthropicMetadataAsync() Assert.NotNull(textContent); var metadata = textContent.Metadata as AnthropicMetadata; Assert.NotNull(metadata); - Assert.Equal(response.FinishReason, metadata.FinishReason); + Assert.Equal(response.StopReason, metadata.FinishReason); Assert.Equal(response.Id, metadata.MessageId); Assert.Equal(response.StopSequence, metadata.StopSequence); Assert.Equal(response.Usage.InputTokens, metadata.InputTokenCount); @@ -128,7 +128,7 @@ public async Task ShouldReturnValidDictionaryMetadataAsync() Assert.NotNull(textContent); var metadata = textContent.Metadata; Assert.NotNull(metadata); - Assert.Equal(response.FinishReason, metadata[nameof(AnthropicMetadata.FinishReason)]); + Assert.Equal(response.StopReason, metadata[nameof(AnthropicMetadata.FinishReason)]); Assert.Equal(response.Id, metadata[nameof(AnthropicMetadata.MessageId)]); Assert.Equal(response.StopSequence, metadata[nameof(AnthropicMetadata.StopSequence)]); Assert.Equal(response.Usage.InputTokens, metadata[nameof(AnthropicMetadata.InputTokenCount)]); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index 3df763acf00d..a28e39946b31 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -74,6 +74,7 @@ internal sealed class AnthropicClient name: $"{s_namespace}.tokens.total", unit: "{token}", description: "Number of tokens used"); + private readonly string? _bearerKey; /// @@ -141,9 +142,9 @@ internal async Task> GenerateChatMessageAsync( try { anthropicResponse = await this.SendRequestAndReturnValidResponseAsync( - this._endpoint, - state.AnthropicRequest, - cancellationToken) + this._endpoint, + state.AnthropicRequest, + cancellationToken) .ConfigureAwait(false); chatResponses = this.GetChatResponseFrom(anthropicResponse); @@ -209,19 +210,21 @@ private AnthropicChatMessageContent GetChatMessageContentFromAnthropicContent(An throw new NotSupportedException($"Content type {content.GetType()} is not supported yet."); } - return new AnthropicChatMessageContent( - role: response.Role, - items: [new TextContent(textContent.Text ?? string.Empty)], - modelId: response.ModelId ?? this._modelId, - innerContent: response, - metadata: GetResponseMetadata(response)); + return new AnthropicChatMessageContent + { + Role = response.Role, + Items = [new TextContent(textContent.Text ?? string.Empty)], + ModelId = response.ModelId ?? this._modelId, + InnerContent = response, + Metadata = GetResponseMetadata(response) + }; } private static AnthropicMetadata GetResponseMetadata(AnthropicResponse response) => new() { MessageId = response.Id, - FinishReason = response.FinishReason, + FinishReason = response.StopReason, StopSequence = response.StopSequence, InputTokenCount = response.Usage?.InputTokens ?? 0, OutputTokenCount = response.Usage?.OutputTokens ?? 0 From 8e0e9fe466be09cc60190e7a55e32985b289c8dc Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Tue, 23 Jul 2024 21:46:27 +0200 Subject: [PATCH 12/19] Adresses feedback --- .../Connectors.Anthropic/AnthropicClientOptions.cs | 5 +++++ dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs index db57f21b64d3..94a789390127 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs @@ -27,6 +27,11 @@ public abstract class ClientOptions /// The target model ID. /// public string? ModelId { get; init; } + + /// + /// Represents the options for configuring the Anthropic client. + /// + protected ClientOptions() { } } /// diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs index 5610a6ab773f..f9553cf9a295 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs @@ -11,7 +11,7 @@ namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; public abstract class TestBase(ITestOutputHelper output) { private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) .AddUserSecrets() .AddEnvironmentVariables() From 82eae1c252274cc654a02e18c090ffb3e124c243 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Tue, 23 Jul 2024 21:56:22 +0200 Subject: [PATCH 13/19] Refactor --- .../Core/AnthropicChatGenerationTests.cs | 2 -- .../Connectors/Connectors.Anthropic/Core/AnthropicClient.cs | 4 +++- .../Connectors.Anthropic/Core/Models/AnthropicRequest.cs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index 91de91bf69c8..d4851d19f9fe 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -54,8 +54,6 @@ public async Task ShouldPassModelIdToRequestContentAsync() public async Task ShouldContainRolesInRequestAsync() { // Arrange - this._messageHandlerStub.ResponseToReturn.Content = new StringContent( - await File.ReadAllTextAsync(ChatTestDataFilePath)); var client = this.CreateChatCompletionClient(); var chatHistory = CreateSampleChatHistory(); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index a28e39946b31..001aacac0796 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -352,7 +352,9 @@ private HttpRequestMessage CreateHttpRequest(object requestData, Uri endpoint) if (!httpRequestMessage.Headers.Contains(HttpHeaderConstant.Names.SemanticKernelVersion)) { - httpRequestMessage.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); + httpRequestMessage.Headers.Add( + HttpHeaderConstant.Names.SemanticKernelVersion, + HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); } if (!httpRequestMessage.Headers.Contains("anthropic-version")) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 5f1518dfae3c..6639737ff70f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -79,6 +79,7 @@ internal sealed class AnthropicRequest [JsonPropertyName("top_k")] public int? TopK { get; set; } + [JsonConstructor] private AnthropicRequest() { } public void AddChatMessage(ChatMessageContent message) From d26b3a103ea4a7eda7547ebf1e55386449727b5d Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 27 Jul 2024 22:24:10 +0100 Subject: [PATCH 14/19] Options update --- .../Core/AnthropicChatGenerationTests.cs | 17 +- ...thropicServiceCollectionExtensionsTests.cs | 23 +-- .../AnthropicChatCompletionServiceTests.cs | 2 +- .../AnthropicClientOptions.cs | 150 ------------------ .../Connectors.Anthropic.csproj | 14 +- .../Core/AnthropicClient.cs | 86 +++++++--- .../Core/Models/AnthropicRequest.cs | 3 +- .../AnthropicKernelBuilderExtensions.cs | 49 +++++- .../AnthropicServiceCollectionExtensions.cs | 49 +++++- .../AnthropicChatMessageContent.cs | 0 .../{ => Contents}/AnthropicFinishReason.cs | 0 .../{ => Contents}/AnthropicMetadata.cs | 0 .../Models/{ => Contents}/AnthropicUsage.cs | 0 .../AmazonBedrockAnthropicClientOptions.cs | 36 +++++ .../Models/Options/AnthropicClientOptions.cs | 40 +++++ .../Models/Options/ClientOptions.cs | 19 +++ .../Options/VertexAIAnthropicClientOptions.cs | 36 +++++ .../AnthropicPromptExecutionSettings.cs | 0 .../AnthropicChatCompletionService.cs | 46 ++++-- .../Connectors/Anthropic/TestBase.cs | 34 +--- 20 files changed, 346 insertions(+), 258 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs rename dotnet/src/Connectors/Connectors.Anthropic/Models/{ => Contents}/AnthropicChatMessageContent.cs (100%) rename dotnet/src/Connectors/Connectors.Anthropic/Models/{ => Contents}/AnthropicFinishReason.cs (100%) rename dotnet/src/Connectors/Connectors.Anthropic/Models/{ => Contents}/AnthropicMetadata.cs (100%) rename dotnet/src/Connectors/Connectors.Anthropic/Models/{ => Contents}/AnthropicUsage.cs (100%) create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AmazonBedrockAnthropicClientOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AnthropicClientOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/Options/ClientOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Models/Options/VertexAIAnthropicClientOptions.cs rename dotnet/src/Connectors/Connectors.Anthropic/{ => Models/Settings}/AnthropicPromptExecutionSettings.cs (100%) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index 91de91bf69c8..b3893914ea6b 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -228,13 +228,7 @@ public async Task ShouldPassVersionToRequestBodyIfCustomHandlerUsedAsync() { // Arrange var options = new AnthropicClientOptions(); - var client = new AnthropicClient( - options: new AnthropicClientOptions - { - ModelId = "fake-model", - Endpoint = new Uri("https://fake-uri.com") - }, - httpClient: this._httpClient); + var client = new AnthropicClient("fake-model", "api-key", options: new(), httpClient: this._httpClient); var chatHistory = CreateSampleChatHistory(); @@ -379,9 +373,8 @@ public async Task ItCreatesRequestWithCustomUriAndCustomHeadersAsync(string head Uri uri = new("https://fake-uri.com"); using var httpHandler = new CustomHeadersHandler(headerName, headerValue); using var httpClient = new HttpClient(httpHandler); - var client = new AnthropicClient( - httpClient: httpClient, - options: new AnthropicClientOptions { ModelId = "fake-model", Endpoint = uri }); + httpClient.BaseAddress = uri; + var client = new AnthropicClient("fake-model", "api-key", options: new(), httpClient: httpClient); var chatHistory = CreateSampleChatHistory(); @@ -409,9 +402,7 @@ private AnthropicClient CreateChatCompletionClient( AnthropicClientOptions? options = null, HttpClient? httpClient = null) { - return new AnthropicClient( - options: new AnthropicClientOptions { ModelId = modelId, ApiKey = apiKey ?? "fake-key" }, - httpClient: httpClient ?? this._httpClient); + return new AnthropicClient(modelId, apiKey ?? "fake-key", options: new(), httpClient: this._httpClient); } private static T? Deserialize(string json) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs index dd356f26e70d..06622e2371dc 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; @@ -21,11 +22,7 @@ public void AnthropicChatCompletionServiceShouldBeRegisteredInKernelServices() var kernelBuilder = Kernel.CreateBuilder(); // Act - kernelBuilder.AddAnthropicChatCompletion(new AnthropicClientOptions - { - ModelId = "modelId", - ApiKey = "apiKey" - }); + kernelBuilder.AddAnthropicChatCompletion("modelId", "apiKey"); var kernel = kernelBuilder.Build(); @@ -42,7 +39,7 @@ public void AnthropicChatCompletionServiceShouldBeRegisteredInServiceCollection( var services = new ServiceCollection(); // Act - services.AddAnthropicChatCompletion(new AnthropicClientOptions() { ModelId = "modelId", ApiKey = "apiKey" }); + services.AddAnthropicChatCompletion("modelId", "apiKey"); var serviceProvider = services.BuildServiceProvider(); // Assert @@ -58,11 +55,8 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInKern var kernelBuilder = Kernel.CreateBuilder(); // Act - kernelBuilder.AddAnthropicChatCompletion(new AnthropicClientOptions - { - ModelId = "modelId", - Endpoint = new Uri("https://example.com") - }); + kernelBuilder.AddAnthropicVertextAIChatCompletion("modelId", bearerTokenProvider: () => ValueTask.FromResult("token"), endpoint: new Uri("https://example.com")); + var kernel = kernelBuilder.Build(); // Assert @@ -78,12 +72,7 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInServ var services = new ServiceCollection(); // Act - services.AddAnthropicChatCompletion( - new AnthropicClientOptions - { - ModelId = "modelId", - Endpoint = new Uri("https://example.com"), - }); + services.AddAnthropicVertexAIChatCompletion("modelId", () => ValueTask.FromResult("token"), endpoint: new Uri("https://example.com")); var serviceProvider = services.BuildServiceProvider(); // Assert diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs index d0b15411b36d..94e8dca76b4f 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Services/AnthropicChatCompletionServiceTests.cs @@ -13,7 +13,7 @@ public void AttributesShouldContainModelId() { // Arrange & Act string model = "fake-model"; - var service = new AnthropicChatCompletionService(new AnthropicClientOptions { ModelId = model, ApiKey = "key" }); + var service = new AnthropicChatCompletionService(model, "key"); // Assert Assert.Equal(model, service.Attributes[AIServiceExtensions.ModelIdKey]); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs deleted file mode 100644 index 94a789390127..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic; - -#pragma warning disable CA1707 // Identifiers should not contain underscores - -/// -/// Represents the options for configuring the Anthropic client. -/// -public abstract class ClientOptions -{ - internal string Version { get; private protected init; } = null!; - - /// - /// The service identifier. - /// - public string? ServiceId { get; init; } - - /// - /// Non-default Anthropic endpoint. - /// - public Uri? Endpoint { get; init; } - - /// - /// The target model ID. - /// - public string? ModelId { get; init; } - - /// - /// Represents the options for configuring the Anthropic client. - /// - protected ClientOptions() { } -} - -/// -/// Represents the options for configuring the Anthropic client with Anthropic provider. -/// -public sealed class AnthropicClientOptions : ClientOptions -{ - private const ServiceVersion LatestVersion = ServiceVersion.V2023_06_01; - - /// - /// The API key for authentication. - /// - public string? ApiKey { get; init; } - - /// The version of the service to use. - public enum ServiceVersion - { - /// Service version "2023-01-01". - V2023_01_01, - - /// Service version "2023-06-01". - V2023_06_01, - } - - /// - /// Initializes new instance of - /// - /// - /// This parameter is optional. - /// Default value is .
- /// - /// Provided version is not supported. - public AnthropicClientOptions(ServiceVersion version = LatestVersion) - { - this.Version = version switch - { - ServiceVersion.V2023_01_01 => "2023-01-01", - ServiceVersion.V2023_06_01 => "2023-06-01", - _ => throw new NotSupportedException("Unsupported service version") - }; - } -} - -/// -/// Represents the options for configuring the Anthropic client with Google VertexAI provider. -/// -public sealed class VertexAIAnthropicClientOptions : ClientOptions -{ - private const ServiceVersion LatestVersion = ServiceVersion.V2023_10_16; - - /// - /// The Bearer key for authentication. - /// - public string? BearerKey { get; init; } - - /// The version of the service to use. - public enum ServiceVersion - { - /// Service version "vertex-2023-10-16". - V2023_10_16, - } - - /// - /// Initializes new instance of - /// - /// - /// This parameter is optional. - /// Default value is .
- /// - /// Provided version is not supported. - public VertexAIAnthropicClientOptions(ServiceVersion version = LatestVersion) - { - this.Version = version switch - { - ServiceVersion.V2023_10_16 => "vertex-2023-10-16", - _ => throw new NotSupportedException("Unsupported service version") - }; - } -} - -/// -/// Represents the options for configuring the Anthropic client with Amazon Bedrock provider. -/// -public sealed class AmazonBedrockAnthropicClientOptions : ClientOptions -{ - private const ServiceVersion LatestVersion = ServiceVersion.V2023_05_31; - - /// - /// The Bearer key for authentication. - /// - public string? BearerKey { get; init; } - - /// The version of the service to use. - public enum ServiceVersion - { - /// Service version "bedrock-2023-05-31". - V2023_05_31, - } - - /// - /// Initializes new instance of - /// - /// - /// This parameter is optional. - /// Default value is .
- /// - /// Provided version is not supported. - public AmazonBedrockAnthropicClientOptions(ServiceVersion version = LatestVersion) - { - this.Version = version switch - { - ServiceVersion.V2023_05_31 => "bedrock-2023-05-31", - _ => throw new NotSupportedException("Unsupported service version") - }; - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Connectors.Anthropic.csproj b/dotnet/src/Connectors/Connectors.Anthropic/Connectors.Anthropic.csproj index d851bca320ff..392a9844d8d4 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Connectors.Anthropic.csproj +++ b/dotnet/src/Connectors/Connectors.Anthropic/Connectors.Anthropic.csproj @@ -6,12 +6,12 @@ $(AssemblyName) netstandard2.0 alpha - SKEXP0001,SKEXP0070 + CA1707,SKEXP0001,SKEXP0070 - - + + @@ -20,13 +20,13 @@ - - + + - - + + \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index a28e39946b31..b79b900f805a 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -16,6 +16,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; @@ -26,6 +27,8 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; internal sealed class AnthropicClient { private const string ModelProvider = "anthropic"; + private readonly Func>? _bearerTokenProvider; + private readonly Dictionary _attributesInternal = new(); internal static JsonSerializerOptions SerializerOptions { get; } = new(JsonOptionsCache.Default) @@ -75,47 +78,78 @@ internal sealed class AnthropicClient unit: "{token}", description: "Number of tokens used"); - private readonly string? _bearerKey; + internal IReadOnlyDictionary Attributes => this._attributesInternal; /// /// Represents a client for interacting with the Anthropic chat completion models. /// + /// Model identifier + /// ApiKey for the client /// Options for the client /// HttpClient instance used to send HTTP requests /// Logger instance used for logging (optional) internal AnthropicClient( - ClientOptions options, + string modelId, + string apiKey, + AnthropicClientOptions options, HttpClient httpClient, ILogger? logger = null) { + Verify.NotNullOrWhiteSpace(modelId); Verify.NotNull(options); Verify.NotNull(httpClient); - Verify.NotNullOrWhiteSpace(options.ModelId); - if (options is AnthropicClientOptions anthropicOptions - && options.Endpoint is null - && httpClient.BaseAddress is null) + Uri targetUri = httpClient.BaseAddress; + if (httpClient.BaseAddress is null) { // If a custom endpoint is not provided, the ApiKey is required - Verify.NotNullOrWhiteSpace(anthropicOptions.ApiKey); - this._apiKey = anthropicOptions.ApiKey; - } - else if (options is VertexAIAnthropicClientOptions vertexOptions) - { - Verify.NotNullOrWhiteSpace(vertexOptions.BearerKey); - this._bearerKey = vertexOptions.BearerKey; - } - else if (options is AmazonBedrockAnthropicClientOptions amazonOptions) - { - Verify.NotNullOrWhiteSpace(amazonOptions.BearerKey); - this._bearerKey = amazonOptions.BearerKey; + Verify.NotNullOrWhiteSpace(apiKey); + this._apiKey = apiKey; + targetUri = new Uri("https://api.anthropic.com/v1/messages"); } this._httpClient = httpClient; this._logger = logger ?? NullLogger.Instance; - this._modelId = options.ModelId; + this._modelId = modelId; + this._version = options.Version; + this._endpoint = targetUri; + + this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + /// Represents a client for interacting with the Anthropic chat completion models. + /// + /// Model identifier + /// Endpoint for the client + /// Bearer token provider + /// Options for the client + /// HttpClient instance used to send HTTP requests + /// Logger instance used for logging (optional) + internal AnthropicClient( + string modelId, + Uri? endpoint, + Func> bearerTokenProvider, + ClientOptions options, + HttpClient httpClient, + ILogger? logger = null) + { this._version = options.Version; - this._endpoint = options.Endpoint ?? httpClient.BaseAddress ?? new Uri("https://api.anthropic.com/v1/messages"); + + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNull(bearerTokenProvider); + Verify.NotNull(options); + Verify.NotNull(httpClient); + + Uri targetUri = endpoint ?? httpClient.BaseAddress + ?? throw new ArgumentException("Endpoint is required if HttpClient.BaseAddress is not set."); + + this._httpClient = httpClient; + this._logger = logger ?? NullLogger.Instance; + this._bearerTokenProvider = bearerTokenProvider; + this._modelId = modelId; + this._version = options?.Version; + this._endpoint = targetUri; } /// @@ -235,7 +269,7 @@ private async Task SendRequestAndReturnValidResponseAsync( AnthropicRequest anthropicRequest, CancellationToken cancellationToken) { - using var httpRequestMessage = this.CreateHttpRequest(anthropicRequest, endpoint); + using var httpRequestMessage = await this.CreateHttpRequestAsync(anthropicRequest, endpoint).ConfigureAwait(false); var body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); var response = DeserializeResponse(body); return response; @@ -342,9 +376,9 @@ private static T DeserializeResponse(string body) } } - private HttpRequestMessage CreateHttpRequest(object requestData, Uri endpoint) + private async Task CreateHttpRequestAsync(object requestData, Uri endpoint) { - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = CreateJsonContent(requestData) }; + var httpRequestMessage = HttpRequest.CreatePostRequest(endpoint, requestData); if (!httpRequestMessage.Headers.Contains("User-Agent")) { httpRequestMessage.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); @@ -364,10 +398,10 @@ private HttpRequestMessage CreateHttpRequest(object requestData, Uri endpoint) { httpRequestMessage.Headers.Add("x-api-key", this._apiKey); } - - if (this._bearerKey is not null && !httpRequestMessage.Headers.Contains("Authorization")) + else + if (this._bearerTokenProvider is not null && !httpRequestMessage.Headers.Contains("Authentication") && await this._bearerTokenProvider().ConfigureAwait(false) is { } bearerKey) { - httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this._bearerKey); + httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerKey); } return httpRequestMessage; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 5f1518dfae3c..12e9238e6a33 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -79,7 +79,8 @@ internal sealed class AnthropicRequest [JsonPropertyName("top_k")] public int? TopK { get; set; } - private AnthropicRequest() { } + [JsonConstructor] + internal AnthropicRequest() { } public void AddChatMessage(ChatMessageContent message) { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index 8b67ece1f7f2..be83569e1260 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Net.Http; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; @@ -18,22 +20,61 @@ public static class AnthropicKernelBuilderExtensions /// Add Anthropic Chat Completion and Text Generation services to the kernel builder. /// /// The kernel builder. + /// Model identifier. + /// API key. /// Optional options for the anthropic client /// The optional custom HttpClient. + /// Service identifier. /// The updated kernel builder. public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, - ClientOptions options, - HttpClient? httpClient = null) + string modelId, + string apiKey, + AnthropicClientOptions? options = null, + HttpClient? httpClient = null, + string? serviceId = null) { Verify.NotNull(builder); - builder.Services.AddKeyedSingleton(options.ServiceId, (serviceProvider, _) => + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( - options: options, + modelId: modelId, + apiKey: apiKey, + options: options ?? new AnthropicClientOptions(), httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), loggerFactory: serviceProvider.GetService())); return builder; } + + /// + /// Add Anthropic Chat Completion and Text Generation services to the kernel builder. + /// + /// The kernel builder. + /// Model identifier. + /// Bearer token provider. + /// Vertex AI Anthropic endpoint. + /// Optional options for the anthropic client + /// Service identifier. + /// The updated kernel builder. + public static IKernelBuilder AddAnthropicVertextAIChatCompletion( + this IKernelBuilder builder, + string modelId, + Func> bearerTokenProvider, + Uri? endpoint = null, + VertexAIAnthropicClientOptions? options = null, + string? serviceId = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AnthropicChatCompletionService( + modelId: modelId, + bearerTokenProvider: bearerTokenProvider, + options: options ?? new VertexAIAnthropicClientOptions(), + httpClient: HttpClientProvider.GetHttpClient(serviceProvider), + loggerFactory: serviceProvider.GetService())); + + return builder; + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 84e9b447664b..04157a5e75a4 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; @@ -14,23 +16,62 @@ namespace Microsoft.SemanticKernel; public static class AnthropicServiceCollectionExtensions { /// - /// Add Anthropic Chat Completion and Text Generation services to the specified service collection. + /// Add Anthropic Chat Completion to the added in service collection. /// - /// The service collection to add the Claude Text Generation service to. + /// The target service collection. + /// Model identifier. + /// API key. /// Optional options for the anthropic client + /// Service identifier. /// The updated service collection. public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, - ClientOptions options) + string modelId, + string apiKey, + AnthropicClientOptions? options = null, + string? serviceId = null) { Verify.NotNull(services); - services.AddKeyedSingleton(options.ServiceId, (serviceProvider, _) => + services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( + modelId: modelId, + apiKey: apiKey, options: options, httpClient: HttpClientProvider.GetHttpClient(serviceProvider), loggerFactory: serviceProvider.GetService())); return services; } + + /// + /// Add Anthropic Chat Completion to the added in service collection. + /// + /// The target service collection. + /// Model identifier. + /// Bearer token provider. + /// Vertex AI Anthropic endpoint. + /// Optional options for the anthropic client + /// Service identifier. + /// The updated service collection. + public static IServiceCollection AddAnthropicVertexAIChatCompletion( + this IServiceCollection services, + string modelId, + Func> bearerTokenProvider, + Uri? endpoint = null, + VertexAIAnthropicClientOptions? options = null, + string? serviceId = null) + { + Verify.NotNull(services); + + services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AnthropicChatCompletionService( + modelId: modelId, + bearerTokenProvider: bearerTokenProvider, + options: options ?? new VertexAIAnthropicClientOptions(), + httpClient: HttpClientProvider.GetHttpClient(serviceProvider), + loggerFactory: serviceProvider.GetService())); + + return services; + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicChatMessageContent.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicChatMessageContent.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicFinishReason.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicFinishReason.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicMetadata.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicMetadata.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicUsage.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicUsage.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicUsage.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AmazonBedrockAnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AmazonBedrockAnthropicClientOptions.cs new file mode 100644 index 000000000000..e9b4d1c4ea99 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AmazonBedrockAnthropicClientOptions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic; + +/// +/// Represents the options for configuring the Anthropic client with Amazon Bedrock provider. +/// +public sealed class AmazonBedrockAnthropicClientOptions : ClientOptions +{ + private const ServiceVersion LatestVersion = ServiceVersion.V2023_05_31; + + /// The version of the service to use. + public enum ServiceVersion + { + /// Service version "bedrock-2023-05-31". + V2023_05_31, + } + + /// + /// Initializes new instance of + /// + /// + /// This parameter is optional. + /// Default value is .
+ /// + /// Provided version is not supported. + public AmazonBedrockAnthropicClientOptions(ServiceVersion version = LatestVersion) : base(version switch + { + ServiceVersion.V2023_05_31 => "bedrock-2023-05-31", + _ => throw new NotSupportedException("Unsupported service version") + }) + { + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AnthropicClientOptions.cs new file mode 100644 index 000000000000..ad070b036b1e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/AnthropicClientOptions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic; + +/// +/// Represents the options for configuring the Anthropic client with Anthropic provider. +/// +public sealed class AnthropicClientOptions : ClientOptions +{ + internal const ServiceVersion LatestVersion = ServiceVersion.V2023_06_01; + + /// The version of the service to use. + public enum ServiceVersion + { + /// Service version "2023-01-01". + V2023_01_01, + + /// Service version "2023-06-01". + V2023_06_01, + } + + /// + /// Initializes new instance of + /// + /// + /// This parameter is optional. + /// Default value is .
+ /// + /// Provided version is not supported. + public AnthropicClientOptions(ServiceVersion version = LatestVersion) : base(version switch + { + ServiceVersion.V2023_01_01 => "2023-01-01", + ServiceVersion.V2023_06_01 => "2023-06-01", + _ => throw new NotSupportedException("Unsupported service version") + }) + { + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/ClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/ClientOptions.cs new file mode 100644 index 000000000000..bd04ee4345e9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/ClientOptions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.Anthropic; + +/// +/// Represents the options for configuring the Anthropic client. +/// +public abstract class ClientOptions +{ + internal string Version { get; init; } + + /// + /// Represents the options for configuring the Anthropic client. + /// + internal protected ClientOptions(string version) + { + this.Version = version; + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/VertexAIAnthropicClientOptions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/VertexAIAnthropicClientOptions.cs new file mode 100644 index 000000000000..4f8075226795 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Options/VertexAIAnthropicClientOptions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic; + +/// +/// Represents the options for configuring the Anthropic client with Google VertexAI provider. +/// +public sealed class VertexAIAnthropicClientOptions : ClientOptions +{ + private const ServiceVersion LatestVersion = ServiceVersion.V2023_10_16; + + /// The version of the service to use. + public enum ServiceVersion + { + /// Service version "vertex-2023-10-16". + V2023_10_16, + } + + /// + /// Initializes new instance of + /// + /// + /// This parameter is optional. + /// Default value is .
+ /// + /// Provided version is not supported. + public VertexAIAnthropicClientOptions(ServiceVersion version = LatestVersion) : base(version switch + { + ServiceVersion.V2023_10_16 => "vertex-2023-10-16", + _ => throw new NotSupportedException("Unsupported service version") + }) + { + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Settings/AnthropicPromptExecutionSettings.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Settings/AnthropicPromptExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index 77bddbfecf3f..358334e6a010 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; @@ -8,7 +9,6 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic.Core; using Microsoft.SemanticKernel.Http; -using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.Anthropic; @@ -17,32 +17,60 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; ///
public sealed class AnthropicChatCompletionService : IChatCompletionService { - private readonly Dictionary _attributesInternal = new(); private readonly AnthropicClient _client; + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + /// /// Initializes a new instance of the class. /// + /// Model identifier. + /// API key. /// Options for the anthropic client /// Optional HTTP client to be used for communication with the Claude API. /// Optional logger factory to be used for logging. public AnthropicChatCompletionService( - ClientOptions options, + string modelId, + string apiKey, + AnthropicClientOptions? options = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - Verify.NotNull(options); - Verify.NotNullOrWhiteSpace(options.ModelId); this._client = new AnthropicClient( + modelId: modelId, + apiKey: apiKey, + options: options ?? new AnthropicClientOptions(), httpClient: HttpClientProvider.GetHttpClient(httpClient), - options: options, logger: loggerFactory?.CreateLogger(typeof(AnthropicChatCompletionService))); - this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, options.ModelId); } - /// - public IReadOnlyDictionary Attributes => this._attributesInternal; + /// + /// Initializes a new instance of the class. + /// + /// Model identifier. + /// Bearer token provider. + /// Options for the anthropic client + /// Claude API endpoint. + /// Optional HTTP client to be used for communication with the Claude API. + /// Optional logger factory to be used for logging. + public AnthropicChatCompletionService( + string modelId, + Func> bearerTokenProvider, + ClientOptions options, + Uri? endpoint = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._client = new AnthropicClient( + modelId: modelId, + bearerTokenProvider: bearerTokenProvider, + options: options, + endpoint: endpoint, + httpClient: HttpClientProvider.GetHttpClient(httpClient), + logger: loggerFactory?.CreateLogger(typeof(AnthropicChatCompletionService))); + } /// public Task> GetChatMessageContentsAsync( diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs index f9553cf9a295..963b719503ee 100644 --- a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; @@ -21,28 +22,9 @@ public abstract class TestBase(ITestOutputHelper output) protected IChatCompletionService GetChatService(ServiceType serviceType) => serviceType switch { - ServiceType.Anthropic => new AnthropicChatCompletionService( - new AnthropicClientOptions - { - ModelId = this.AnthropicGetModel(), - ApiKey = this.AnthropicGetApiKey() - }), - - ServiceType.VertexAI => new AnthropicChatCompletionService( - new VertexAIAnthropicClientOptions - { - ModelId = this.VertexAIGetModel(), - Endpoint = new Uri(this.VertexAIGetEndpoint()), - BearerKey = this.VertexAIGetBearerKey() - }), - - ServiceType.AmazonBedrock => new AnthropicChatCompletionService( - new AmazonBedrockAnthropicClientOptions - { - ModelId = this.AmazonBedrockGetModel(), - Endpoint = new Uri(this.AmazonBedrockGetEndpoint()), - BearerKey = this.AmazonBedrockGetBearerKey() // TODO: setup aws bedrock claude - }), + ServiceType.Anthropic => new AnthropicChatCompletionService(this.AnthropicGetModel(), this.AnthropicGetApiKey(), new()), + ServiceType.VertexAI => new AnthropicChatCompletionService(this.VertexAIGetModel(), this.VertexAIGetBearerKey(), new VertexAIAnthropicClientOptions(), this.VertexAIGetEndpoint()), + ServiceType.AmazonBedrock => new AnthropicChatCompletionService(this.VertexAIGetModel(), this.AmazonBedrockGetBearerKey(), new AmazonBedrockAnthropicClientOptions(), this.VertexAIGetEndpoint()), _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null) }; @@ -56,9 +38,9 @@ public enum ServiceType private string AnthropicGetModel() => this._configuration.GetSection("Anthropic:ModelId").Get()!; private string AnthropicGetApiKey() => this._configuration.GetSection("Anthropic:ApiKey").Get()!; private string VertexAIGetModel() => this._configuration.GetSection("VertexAI:Anthropic:ModelId").Get()!; - private string VertexAIGetEndpoint() => this._configuration.GetSection("VertexAI:Anthropic:Endpoint").Get()!; - private string VertexAIGetBearerKey() => this._configuration.GetSection("VertexAI:BearerKey").Get()!; - private string AmazonBedrockGetBearerKey() => this._configuration.GetSection("AmazonBedrock:Anthropic:BearerKey").Get()!; + private Uri VertexAIGetEndpoint() => new(this._configuration.GetSection("VertexAI:Anthropic:Endpoint").Get()!); + private Func> VertexAIGetBearerKey() => () => ValueTask.FromResult(this._configuration.GetSection("VertexAI:BearerKey").Get()!); + private Func> AmazonBedrockGetBearerKey() => () => ValueTask.FromResult(this._configuration.GetSection("AmazonBedrock:Anthropic:BearerKey").Get()!); private string AmazonBedrockGetModel() => this._configuration.GetSection("AmazonBedrock:Anthropic:ModelId").Get()!; - private string AmazonBedrockGetEndpoint() => this._configuration.GetSection("AmazonBedrock:Anthropic:Endpoint").Get()!; + private Uri AmazonBedrockGetEndpoint() => new(this._configuration.GetSection("AmazonBedrock:Anthropic:Endpoint").Get()!); } From 2276814c8ae016e8adb12f0653d8b881e9af98c5 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Sun, 28 Jul 2024 15:29:24 +0200 Subject: [PATCH 15/19] Fix errors. Format. --- .../Core/AnthropicChatGenerationTests.cs | 21 +++++++++++++++++++ .../Core/Models/AnthropicRequest.cs | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index d4851d19f9fe..80f7b52f3075 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -69,6 +69,27 @@ public async Task ShouldContainRolesInRequestAsync() item => Assert.Equal(chatHistory[3].Role, item.Role)); } + [Fact] + public async Task ShouldContainMessagesInRequestAsync() + { + // Arrange + var client = this.CreateChatCompletionClient(); + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + AnthropicRequest? request = Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(request); + Assert.Collection(request.Messages, + item => Assert.Equal(chatHistory[1].Content, GetTextFrom(item.Contents[0])), + item => Assert.Equal(chatHistory[2].Content, GetTextFrom(item.Contents[0])), + item => Assert.Equal(chatHistory[3].Content, GetTextFrom(item.Contents[0]))); + + string? GetTextFrom(AnthropicContent content) => ((AnthropicTextContent)content).Text; + } + [Fact] public async Task ShouldReturnValidChatResponseAsync() { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 6639737ff70f..95bf87509173 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -25,7 +25,7 @@ internal sealed class AnthropicRequest /// from the content in that message. This can be used to constrain part of the model's response. ///
[JsonPropertyName("messages")] - public IList Messages { get; } = new List(); + public IList Messages { get; init; } = new List(); [JsonPropertyName("model")] public string ModelId { get; set; } = null!; From 9fa578bd9c1f7e670179b86f6653f75ecb41c0b9 Mon Sep 17 00:00:00 2001 From: Krzysztof Kasprowicz Date: Sun, 28 Jul 2024 15:38:46 +0200 Subject: [PATCH 16/19] Refactor Message property initialization Updated the Messages property to use JsonObjectCreationHandling.Populate for better control over object instantiation. This change ensures that existing instances are populated rather than replaced. --- .../Connectors.Anthropic/Core/Models/AnthropicRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 95bf87509173..a69e5008ec7a 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -25,7 +25,8 @@ internal sealed class AnthropicRequest /// from the content in that message. This can be used to constrain part of the model's response. /// [JsonPropertyName("messages")] - public IList Messages { get; init; } = new List(); + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public IList Messages { get; } = new List(); [JsonPropertyName("model")] public string ModelId { get; set; } = null!; From 8f1371b114ea40c2bd77e1178ceace8bf5081b3c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:42:19 +0100 Subject: [PATCH 17/19] Fix unit tests --- .../Core/Models/Message/AnthropicContent.cs | 18 +++++++++++++++--- .../Message/AnthropicDeltaJsonContent.cs | 15 --------------- .../Message/AnthropicDeltaTextContent.cs | 15 --------------- .../Models/Message/AnthropicImageContent.cs | 7 +++++++ .../Models/Message/AnthropicTextContent.cs | 7 +++++++ .../AnthropicKernelBuilderExtensions.cs | 1 + .../AnthropicServiceCollectionExtensions.cs | 1 + 7 files changed, 31 insertions(+), 33 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index d72cffcac3df..5ff38a30cd9c 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -1,12 +1,24 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; + namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// /// Represents the request/response content of Anthropic. /// [InternalJsonDerived(typeof(AnthropicTextContent), typeDiscriminator: "text")] -[InternalJsonDerived(typeof(AnthropicDeltaTextContent), typeDiscriminator: "text_delta")] -[InternalJsonDerived(typeof(AnthropicDeltaJsonContent), typeDiscriminator: "input_json_delta")] [InternalJsonDerived(typeof(AnthropicImageContent), typeDiscriminator: "image")] -internal abstract class AnthropicContent; +internal abstract class AnthropicContent +{ + [JsonConstructor] + internal protected AnthropicContent(string type) + { + this.Type = type; + } + /// + /// Currently supported only base64. + /// + [JsonPropertyName("type")] + public string Type { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs deleted file mode 100644 index d31e7b183c38..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaJsonContent.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class AnthropicDeltaJsonContent : AnthropicContent -{ - /// - /// Only used when type is "input_json_delta". The partial json content. - /// - [JsonRequired] - [JsonPropertyName("partial_json")] - public string? PartialJson { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs deleted file mode 100644 index 701e43d73433..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicDeltaTextContent.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class AnthropicDeltaTextContent : AnthropicContent -{ - /// - /// Only used when type is "text". The text content. - /// - [JsonRequired] - [JsonPropertyName("text")] - public string? Text { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs index 8953b1876a97..de65e3460724 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs @@ -12,6 +12,13 @@ internal sealed class AnthropicImageContent : AnthropicContent [JsonPropertyName("source")] public SourceEntity? Source { get; set; } + /// + /// Initializes a new instance of the class. + /// + public AnthropicImageContent() : base("image") + { + } + internal sealed class SourceEntity { /// diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs index 9000100161c2..d066d25dfaf8 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs @@ -12,4 +12,11 @@ internal sealed class AnthropicTextContent : AnthropicContent [JsonRequired] [JsonPropertyName("text")] public string? Text { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public AnthropicTextContent() : base("text") + { + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index be83569e1260..dbd70a2ca5db 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -72,6 +72,7 @@ public static IKernelBuilder AddAnthropicVertextAIChatCompletion( modelId: modelId, bearerTokenProvider: bearerTokenProvider, options: options ?? new VertexAIAnthropicClientOptions(), + endpoint: endpoint, httpClient: HttpClientProvider.GetHttpClient(serviceProvider), loggerFactory: serviceProvider.GetService())); diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs index 04157a5e75a4..83ed98bfafcf 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -68,6 +68,7 @@ public static IServiceCollection AddAnthropicVertexAIChatCompletion( new AnthropicChatCompletionService( modelId: modelId, bearerTokenProvider: bearerTokenProvider, + endpoint: endpoint, options: options ?? new VertexAIAnthropicClientOptions(), httpClient: HttpClientProvider.GetHttpClient(serviceProvider), loggerFactory: serviceProvider.GetService())); From 0a013954fd0a8ae311428130b854a69f3d5ee17b Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:11:46 +0100 Subject: [PATCH 18/19] Removing complex polimorphyc logic --- .../Core/AnthropicChatGenerationTests.cs | 4 +- .../Core/AnthropicRequestTests.cs | 32 +++++----- .../Core/AnthropicClient.cs | 18 ++---- .../Core/Models/AnthropicRequest.cs | 8 +-- .../Core/Models/Message/AnthropicContent.cs | 53 +++++++++++++---- .../Models/Message/AnthropicImageContent.cs | 42 ------------- .../Models/Message/AnthropicTextContent.cs | 22 ------- .../Message/InternalJsonDerivedAttribute.cs | 16 ----- .../Message/JsonTypeDiscriminatorHelper.cs | 40 ------------- .../Message/PolymorphicJsonConverter.cs | 59 ------------------- .../PolymorphicJsonConverterFactory.cs | 23 -------- .../AnthropicChatCompletionService.cs | 1 - 12 files changed, 69 insertions(+), 249 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs delete mode 100644 dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index b3893914ea6b..4167149a1283 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -407,12 +407,12 @@ private AnthropicClient CreateChatCompletionClient( private static T? Deserialize(string json) { - return JsonSerializer.Deserialize(json, options: AnthropicClient.SerializerOptions); + return JsonSerializer.Deserialize(json); } private static T? Deserialize(ReadOnlySpan json) { - return JsonSerializer.Deserialize(json, options: AnthropicClient.SerializerOptions); + return JsonSerializer.Deserialize(json); } public void Dispose() diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs index e0af3634a194..d7925f4652bd 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs @@ -82,11 +82,11 @@ public void FromChatHistoryItReturnsWithChatHistory() var request = AnthropicRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings); // Assert - Assert.All(request.Messages, c => Assert.IsType(c.Contents[0])); + Assert.All(request.Messages, c => Assert.IsType(c.Contents[0])); Assert.Collection(request.Messages, - c => Assert.Equal(chatHistory[0].Content, ((AnthropicTextContent)c.Contents[0]).Text), - c => Assert.Equal(chatHistory[1].Content, ((AnthropicTextContent)c.Contents[0]).Text), - c => Assert.Equal(chatHistory[2].Content, ((AnthropicTextContent)c.Contents[0]).Text)); + c => Assert.Equal(chatHistory[0].Content, ((AnthropicContent)c.Contents[0]).Text), + c => Assert.Equal(chatHistory[1].Content, ((AnthropicContent)c.Contents[0]).Text), + c => Assert.Equal(chatHistory[2].Content, ((AnthropicContent)c.Contents[0]).Text)); Assert.Collection(request.Messages, c => Assert.Equal(chatHistory[0].Role, c.Role), c => Assert.Equal(chatHistory[1].Role, c.Role), @@ -111,11 +111,11 @@ public void FromChatHistoryTextAsTextContentItReturnsWithChatHistory() var request = AnthropicRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings); // Assert - Assert.All(request.Messages, c => Assert.IsType(c.Contents[0])); + Assert.All(request.Messages, c => Assert.IsType(c.Contents[0])); Assert.Collection(request.Messages, - c => Assert.Equal(chatHistory[0].Content, ((AnthropicTextContent)c.Contents[0]).Text), - c => Assert.Equal(chatHistory[1].Content, ((AnthropicTextContent)c.Contents[0]).Text), - c => Assert.Equal(chatHistory[2].Items.Cast().Single().Text, ((AnthropicTextContent)c.Contents[0]).Text)); + c => Assert.Equal(chatHistory[0].Content, ((AnthropicContent)c.Contents[0]).Text), + c => Assert.Equal(chatHistory[1].Content, ((AnthropicContent)c.Contents[0]).Text), + c => Assert.Equal(chatHistory[2].Items.Cast().Single().Text, ((AnthropicContent)c.Contents[0]).Text)); } [Fact] @@ -139,16 +139,16 @@ public void FromChatHistoryImageAsImageContentItReturnsWithChatHistory() // Assert Assert.Collection(request.Messages, - c => Assert.IsType(c.Contents[0]), - c => Assert.IsType(c.Contents[0]), - c => Assert.IsType(c.Contents[0])); + c => Assert.IsType(c.Contents[0]), + c => Assert.IsType(c.Contents[0]), + c => Assert.IsType(c.Contents[0])); Assert.Collection(request.Messages, - c => Assert.Equal(chatHistory[0].Content, ((AnthropicTextContent)c.Contents[0]).Text), - c => Assert.Equal(chatHistory[1].Content, ((AnthropicTextContent)c.Contents[0]).Text), + c => Assert.Equal(chatHistory[0].Content, ((AnthropicContent)c.Contents[0]).Text), + c => Assert.Equal(chatHistory[1].Content, ((AnthropicContent)c.Contents[0]).Text), c => { - Assert.Equal(chatHistory[2].Items.Cast().Single().MimeType, ((AnthropicImageContent)c.Contents[0]).Source!.MediaType); - Assert.True(imageAsBytes.ToArray().SequenceEqual(Convert.FromBase64String(((AnthropicImageContent)c.Contents[0]).Source!.Data!))); + Assert.Equal(chatHistory[2].Items.Cast().Single().MimeType, ((AnthropicContent)c.Contents[0]).Source!.MediaType); + Assert.True(imageAsBytes.ToArray().SequenceEqual(Convert.FromBase64String(((AnthropicContent)c.Contents[0]).Source!.Data!))); }); } @@ -216,7 +216,7 @@ public void AddChatMessageToRequestItAddsChatMessage() // Assert Assert.Single(request.Messages, - c => c.Contents[0] is AnthropicTextContent content && string.Equals(message.Content, content.Text, StringComparison.Ordinal)); + c => c.Contents[0] is AnthropicContent content && string.Equals(message.Content, content.Text, StringComparison.Ordinal)); Assert.Single(request.Messages, c => Equals(message.Role, c.Role)); } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs index b79b900f805a..811a09e215d1 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -17,7 +17,6 @@ using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.Text; namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; @@ -30,13 +29,6 @@ internal sealed class AnthropicClient private readonly Func>? _bearerTokenProvider; private readonly Dictionary _attributesInternal = new(); - internal static JsonSerializerOptions SerializerOptions { get; } - = new(JsonOptionsCache.Default) - { - Converters = { new PolymorphicJsonConverterFactory() }, - TypeInfoResolver = JsonTypeDiscriminatorHelper.TypeInfoResolver - }; - private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly string _modelId; @@ -239,15 +231,15 @@ private List GetChatMessageContentsFromResponse(Ant private AnthropicChatMessageContent GetChatMessageContentFromAnthropicContent(AnthropicResponse response, AnthropicContent content) { - if (content is not AnthropicTextContent textContent) + if (!string.Equals(content.Type, "text", StringComparison.OrdinalIgnoreCase)) { - throw new NotSupportedException($"Content type {content.GetType()} is not supported yet."); + throw new NotSupportedException($"Content type {content.Type} is not supported yet."); } return new AnthropicChatMessageContent { Role = response.Role, - Items = [new TextContent(textContent.Text ?? string.Empty)], + Items = [new TextContent(content.Text ?? string.Empty)], ModelId = response.ModelId ?? this._modelId, InnerContent = response, Metadata = GetResponseMetadata(response) @@ -365,7 +357,7 @@ private static T DeserializeResponse(string body) { try { - return JsonSerializer.Deserialize(body, options: SerializerOptions) ?? throw new JsonException("Response is null"); + return JsonSerializer.Deserialize(body) ?? throw new JsonException("Response is null"); } catch (JsonException exc) { @@ -414,7 +406,7 @@ private async Task CreateHttpRequestAsync(object requestData { byte[] utf8Bytes = payload is string s ? Encoding.UTF8.GetBytes(s) - : JsonSerializer.SerializeToUtf8Bytes(payload, SerializerOptions); + : JsonSerializer.SerializeToUtf8Bytes(payload); content = new ByteArrayContent(utf8Bytes); content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs index 12e9238e6a33..cec43a1531b9 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -25,7 +25,7 @@ internal sealed class AnthropicRequest /// from the content in that message. This can be used to constrain part of the model's response. /// [JsonPropertyName("messages")] - public IList Messages { get; } = new List(); + public IList Messages { get; set; } = []; [JsonPropertyName("model")] public string ModelId { get; set; } = null!; @@ -146,12 +146,12 @@ private static List CreateAnthropicMessages(ChatMessageContent private static AnthropicContent GetAnthropicMessageFromKernelContent(KernelContent content) => content switch { - TextContent textContent => new AnthropicTextContent { Text = textContent.Text ?? string.Empty }, + TextContent textContent => new AnthropicContent("text") { Text = textContent.Text ?? string.Empty }, ImageContent imageContent => CreateAnthropicImageContent(imageContent), _ => throw new NotSupportedException($"Content type '{content.GetType().Name}' is not supported.") }; - private static AnthropicImageContent CreateAnthropicImageContent(ImageContent imageContent) + private static AnthropicContent CreateAnthropicImageContent(ImageContent imageContent) { var dataUri = DataUriParser.Parse(imageContent.DataUri); if (dataUri.DataFormat?.Equals("base64", StringComparison.OrdinalIgnoreCase) != true) @@ -159,7 +159,7 @@ private static AnthropicImageContent CreateAnthropicImageContent(ImageContent im throw new InvalidOperationException("Image content must be base64 encoded."); } - return new AnthropicImageContent + return new AnthropicContent("image") { Source = new() { diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs index 5ff38a30cd9c..fab9f2b380f1 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicContent.cs @@ -4,21 +4,52 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; -/// -/// Represents the request/response content of Anthropic. -/// -[InternalJsonDerived(typeof(AnthropicTextContent), typeDiscriminator: "text")] -[InternalJsonDerived(typeof(AnthropicImageContent), typeDiscriminator: "image")] -internal abstract class AnthropicContent +internal sealed class AnthropicContent { - [JsonConstructor] - internal protected AnthropicContent(string type) - { - this.Type = type; - } /// /// Currently supported only base64. /// [JsonPropertyName("type")] public string Type { get; set; } + + /// + /// When type is "text", the text content. + /// + [JsonPropertyName("text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Text { get; set; } + + /// + /// When type is "image", the source of the image. + /// + [JsonPropertyName("source")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SourceEntity? Source { get; set; } + + [JsonConstructor] + public AnthropicContent(string type) + { + this.Type = type; + } + + internal sealed class SourceEntity + { + /// + /// Currently supported only base64. + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// The media type of the image. + /// + [JsonPropertyName("media_type")] + public string? MediaType { get; set; } + + /// + /// The base64 encoded image data. + /// + [JsonPropertyName("data")] + public string? Data { get; set; } + } } diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs deleted file mode 100644 index de65e3460724..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class AnthropicImageContent : AnthropicContent -{ - /// - /// Only used when type is "image". The image content. - /// - [JsonPropertyName("source")] - public SourceEntity? Source { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public AnthropicImageContent() : base("image") - { - } - - internal sealed class SourceEntity - { - /// - /// Currently supported only base64. - /// - [JsonPropertyName("type")] - public string? Type { get; set; } - - /// - /// The media type of the image. - /// - [JsonPropertyName("media_type")] - public string? MediaType { get; set; } - - /// - /// The base64 encoded image data. - /// - [JsonPropertyName("data")] - public string? Data { get; set; } - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs deleted file mode 100644 index d066d25dfaf8..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class AnthropicTextContent : AnthropicContent -{ - /// - /// Only used when type is "text". The text content. - /// - [JsonRequired] - [JsonPropertyName("text")] - public string? Text { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public AnthropicTextContent() : base("text") - { - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs deleted file mode 100644 index 8a915a5394ed..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/InternalJsonDerivedAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -/// -/// Same as but used to avoid NotSupportedExceptions when using the former. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] -internal sealed class InternalJsonDerivedAttribute(Type subtype, string typeDiscriminator) : Attribute -{ - public Type Subtype { get; internal set; } = subtype; - public string TypeDiscriminator { get; internal set; } = typeDiscriminator; -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs deleted file mode 100644 index e42e49f7716a..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/JsonTypeDiscriminatorHelper.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Reflection; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -// Temporary solution from https://github.com/dotnet/runtime/issues/72604 -// TODO: Remove this once we move to .NET 9 - -internal static class JsonTypeDiscriminatorHelper -{ - internal static IJsonTypeInfoResolver TypeInfoResolver { get; } = new DefaultJsonTypeInfoResolver - { - Modifiers = - { - static typeInfo => - { - var propertyNamingPolicy = typeInfo.Options.PropertyNamingPolicy; - - // Temporary hack to ensure subclasses of abstract classes will always include the type field - if (typeInfo.Type.BaseType is { IsAbstract: true } && - typeInfo.Type.BaseType.GetCustomAttributes().Any()) - { - var discriminatorPropertyName = propertyNamingPolicy?.ConvertName("type") ?? "type"; - if (typeInfo.Properties.All(p => p.Name != discriminatorPropertyName)) - { - var discriminatorValue = typeInfo.Type.BaseType - .GetCustomAttributes() - .First(attr => attr.Subtype == typeInfo.Type).TypeDiscriminator; - var propInfo = typeInfo.CreateJsonPropertyInfo(typeof(string), discriminatorPropertyName); - propInfo.Get = _ => discriminatorValue; - typeInfo.Properties.Insert(0, propInfo); - } - } - }, - }, - }; -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs deleted file mode 100644 index 36696e554b29..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverter.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -/// -/// A temporary hack to support deserializing JSON payloads that use polymorphism but don't specify type as the first field. -/// Modified from https://github.com/dotnet/runtime/issues/72604#issuecomment-1440708052. -/// -internal sealed class PolymorphicJsonConverter : JsonConverter -{ - private readonly string _discriminatorPropName; - private readonly Dictionary _discriminatorToSubtype = []; - - public PolymorphicJsonConverter(JsonSerializerOptions options) - { - this._discriminatorPropName = options.PropertyNamingPolicy?.ConvertName("type") ?? "type"; - foreach (var subtype in typeof(T).GetCustomAttributes()) - { - this._discriminatorToSubtype.Add(subtype.TypeDiscriminator, subtype.Subtype); - } - } - - public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert; - - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var reader2 = reader; - using var doc = JsonDocument.ParseValue(ref reader2); - - var root = doc.RootElement; - var typeField = root.GetProperty(this._discriminatorPropName); - - if (typeField.GetString() is not { } typeName) - { - throw new JsonException( - $"Could not find string property {this._discriminatorPropName} " + - $"when trying to deserialize {typeof(T).Name}"); - } - - if (!this._discriminatorToSubtype.TryGetValue(typeName, out var type)) - { - throw new JsonException($"Unknown type: {typeName}"); - } - - return (T)JsonSerializer.Deserialize(ref reader, type, options)!; - } - - public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) - { - var type = value!.GetType(); - JsonSerializer.Serialize(writer, value, type, options); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs deleted file mode 100644 index 199c55d18585..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/PolymorphicJsonConverterFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -internal sealed class PolymorphicJsonConverterFactory : JsonConverterFactory -{ - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert.IsAbstract && typeToConvert.GetCustomAttributes().Any(); - } - - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - return (JsonConverter?)Activator.CreateInstance( - typeof(PolymorphicJsonConverter<>).MakeGenericType(typeToConvert), options); - } -} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index 358334e6a010..ac52bde8aeaf 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -37,7 +37,6 @@ public AnthropicChatCompletionService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._client = new AnthropicClient( modelId: modelId, apiKey: apiKey, From 32616f6bb1095483a0ddbd6369d173d0dc48d902 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:17:52 +0100 Subject: [PATCH 19/19] Warning fixes --- .../Core/AnthropicChatGenerationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs index 4ecc37e56533..7b9ce14ad150 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -87,7 +87,7 @@ public async Task ShouldContainMessagesInRequestAsync() item => Assert.Equal(chatHistory[2].Content, GetTextFrom(item.Contents[0])), item => Assert.Equal(chatHistory[3].Content, GetTextFrom(item.Contents[0]))); - string? GetTextFrom(AnthropicContent content) => ((AnthropicTextContent)content).Text; + string? GetTextFrom(AnthropicContent content) => ((AnthropicContent)content).Text; } [Fact]