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..7b9ce14ad150 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicChatGenerationTests.cs @@ -0,0 +1,478 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +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; +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 + 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 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) => ((AnthropicContent)content).Text; + } + + [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.StopReason, 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.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)]); + 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 ShouldPassVersionToRequestBodyIfCustomHandlerUsedAsync() + { + // Arrange + var options = new AnthropicClientOptions(); + var client = new AnthropicClient("fake-model", "api-key", options: new(), httpClient: this._httpClient); + + 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() + { + // 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 ItCreatesRequestWithValidUserAgentAsync() + { + // 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 ItCreatesRequestWithSemanticKernelVersionHeaderAsync() + { + // 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 ItCreatesRequestWithValidAnthropicVersionAsync() + { + // 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 ItCreatesRequestWithValidApiKeyAsync() + { + // 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 ItCreatesRequestWithJsonContentTypeAsync() + { + // 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()); + } + + [Theory] + [InlineData("custom-header", "custom-value")] + public async Task ItCreatesRequestWithCustomUriAndCustomHeadersAsync(string headerName, string headerValue) + { + // Arrange + Uri uri = new("https://fake-uri.com"); + using var httpHandler = new CustomHeadersHandler(headerName, headerValue); + using var httpClient = new HttpClient(httpHandler); + httpClient.BaseAddress = uri; + var client = new AnthropicClient("fake-model", "api-key", options: new(), httpClient: httpClient); + + var chatHistory = CreateSampleChatHistory(); + + // Act + await client.GenerateChatMessageAsync(chatHistory); + + // Assert + Assert.Equal(uri, httpHandler.RequestUri); + Assert.NotNull(httpHandler.RequestHeaders); + Assert.Equal(headerValue, httpHandler.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(modelId, apiKey ?? "fake-key", options: new(), httpClient: this._httpClient); + } + + private static T? Deserialize(string json) + { + return JsonSerializer.Deserialize(json); + } + + private static T? Deserialize(ReadOnlySpan json) + { + return JsonSerializer.Deserialize(json); + } + + 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 fbc05591c9c1..d7925f4652bd 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Core/AnthropicRequestTests.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Nodes; +using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Anthropic; @@ -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 = []; @@ -82,11 +82,11 @@ public void FromChatHistoryItReturnsClaudeRequestWithChatHistory() 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), @@ -94,7 +94,7 @@ public void FromChatHistoryItReturnsClaudeRequestWithChatHistory() } [Fact] - public void FromChatHistoryTextAsTextContentItReturnsClaudeRequestWithChatHistory() + public void FromChatHistoryTextAsTextContentItReturnsWithChatHistory() { // Arrange ChatHistory chatHistory = []; @@ -111,15 +111,15 @@ public void FromChatHistoryTextAsTextContentItReturnsClaudeRequestWithChatHistor 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] - public void FromChatHistoryImageAsImageContentItReturnsClaudeRequestWithChatHistory() + public void FromChatHistoryImageAsImageContentItReturnsWithChatHistory() { // Arrange ReadOnlyMemory imageAsBytes = new byte[] { 0x00, 0x01, 0x02, 0x03 }; @@ -139,16 +139,16 @@ public void FromChatHistoryImageAsImageContentItReturnsClaudeRequestWithChatHist // 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!))); }); } @@ -174,135 +174,56 @@ 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 + { + Role = AuthorRole.User, + Items = [new TextContent("user-message")], + ModelId = "model-id", + Encoding = Encoding.UTF8 + }; // Act request.AddChatMessage(message); // 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)); } - 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/Extensions/AnthropicServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Anthropic.UnitTests/Extensions/AnthropicServiceCollectionExtensionsTests.cs index 69b79a5d9283..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; @@ -22,6 +23,7 @@ public void AnthropicChatCompletionServiceShouldBeRegisteredInKernelServices() // Act kernelBuilder.AddAnthropicChatCompletion("modelId", "apiKey"); + var kernel = kernelBuilder.Build(); // Assert @@ -53,7 +55,8 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInKern var kernelBuilder = Kernel.CreateBuilder(); // Act - kernelBuilder.AddAnthropicChatCompletion("modelId", new Uri("https://example.com"), null); + kernelBuilder.AddAnthropicVertextAIChatCompletion("modelId", bearerTokenProvider: () => ValueTask.FromResult("token"), endpoint: new Uri("https://example.com")); + var kernel = kernelBuilder.Build(); // Assert @@ -69,7 +72,7 @@ public void AnthropicChatCompletionServiceCustomEndpointShouldBeRegisteredInServ var services = new ServiceCollection(); // Act - services.AddAnthropicChatCompletion("modelId", new Uri("https://example.com"), null); + 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/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 deleted file mode 100644 index 1bbcecf1fcae..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicClientOptions.cs +++ /dev/null @@ -1,40 +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 sealed class AnthropicClientOptions -{ - 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, - - /// Service version "2023-06-01". - V2023_06_01 = 2, - } - - internal string Version { get; } - - /// Initializes new instance of OpenAIClientOptions. - 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") - }; - } -} 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/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 a73783c4d942..7f896389baca 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/AnthropicClient.cs @@ -2,15 +2,21 @@ using System; using System.Collections.Generic; +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; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Services; namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; @@ -19,68 +25,123 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; /// internal sealed class AnthropicClient { + private const string ModelProvider = "anthropic"; + private readonly Func>? _bearerTokenProvider; + private readonly Dictionary _attributesInternal = new(); + 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 AnthropicClientOptions _options; + private readonly string? _version; + + 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"); + + internal IReadOnlyDictionary Attributes => this._attributesInternal; /// /// 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 + /// Model identifier + /// ApiKey for the client /// Options for the client + /// HttpClient instance used to send HTTP requests /// Logger instance used for logging (optional) - public AnthropicClient( - HttpClient httpClient, + internal AnthropicClient( string modelId, string apiKey, - AnthropicClientOptions? options, + AnthropicClientOptions options, + HttpClient httpClient, ILogger? logger = null) { - Verify.NotNull(httpClient); Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); + Verify.NotNull(options); + Verify.NotNull(httpClient); + + Uri targetUri = httpClient.BaseAddress; + if (httpClient.BaseAddress is null) + { + // If a custom endpoint is not provided, the ApiKey is required + 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 = modelId; - this._apiKey = apiKey; - this._options = options ?? new AnthropicClientOptions(); - this._endpoint = new Uri("https://api.anthropic.com/v1/messages"); + this._version = options.Version; + this._endpoint = targetUri; + + this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); } /// /// 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 + /// 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) - public AnthropicClient( - HttpClient httpClient, + internal AnthropicClient( string modelId, - Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options, + Uri? endpoint, + Func> bearerTokenProvider, + ClientOptions options, + HttpClient httpClient, ILogger? logger = null) { - Verify.NotNull(httpClient); + this._version = options.Version; + Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNull(endpoint); + 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._endpoint = endpoint; - this._customRequestHandler = requestHandler; - this._options = options ?? new AnthropicClientOptions(); + this._version = options?.Version; + this._endpoint = targetUri; } /// @@ -91,14 +152,148 @@ 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, 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 { 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); + + 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) + => response.Contents.Select(content => this.GetChatMessageContentFromAnthropicContent(response, content)).ToList(); + + private AnthropicChatMessageContent GetChatMessageContentFromAnthropicContent(AnthropicResponse response, AnthropicContent content) + { + if (!string.Equals(content.Type, "text", StringComparison.OrdinalIgnoreCase)) + { + throw new NotSupportedException($"Content type {content.Type} is not supported yet."); + } + + return new AnthropicChatMessageContent + { + Role = response.Role, + Items = [new TextContent(content.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.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); + var body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); + var response = DeserializeResponse(body); + return response; + } + + private ChatCompletionState ValidateInputAndCreateChatCompletionState( + ChatHistory chatHistory, + PromptExecutionSettings? executionSettings) + { + ValidateChatHistory(chatHistory); + + var anthropicExecutionSettings = AnthropicPromptExecutionSettings.FromExecutionSettings(executionSettings); + ValidateMaxTokens(anthropicExecutionSettings.MaxTokens); + anthropicExecutionSettings.ModelId ??= this._modelId; + + this.Log(LogLevel.Trace, "ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chatHistory), + JsonSerializer.Serialize(anthropicExecutionSettings)); + + var filteredChatHistory = new ChatHistory(chatHistory.Where(IsAssistantOrUserOrSystem)); + var anthropicRequest = AnthropicRequest.FromChatHistoryAndExecutionSettings(filteredChatHistory, anthropicExecutionSettings); + anthropicRequest.Version = this._version; + + return new ChatCompletionState + { + ChatHistory = chatHistory, + ExecutionSettings = anthropicExecutionSettings, + AnthropicRequest = anthropicRequest + }; + + static bool IsAssistantOrUserOrSystem(ChatMessageContent msg) + => msg.Role == AuthorRole.Assistant || msg.Role == AuthorRole.User || msg.Role == AuthorRole.System; } /// @@ -109,7 +304,7 @@ public async Task> GenerateChatMessageAsync( /// 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, @@ -129,6 +324,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) @@ -167,19 +371,53 @@ private static T DeserializeResponse(string body) private async Task CreateHttpRequestAsync(object requestData, Uri endpoint) { var httpRequestMessage = HttpRequest.CreatePostRequest(endpoint, 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 (!httpRequestMessage.Headers.Contains(HttpHeaderConstant.Names.SemanticKernelVersion)) + { + httpRequestMessage.Headers.Add( + HttpHeaderConstant.Names.SemanticKernelVersion, + HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AnthropicClient))); + } + + if (!httpRequestMessage.Headers.Contains("anthropic-version")) + { + httpRequestMessage.Headers.Add("anthropic-version", this._version); + } - if (this._customRequestHandler != null) + if (this._apiKey is not null && !httpRequestMessage.Headers.Contains("x-api-key")) + { + httpRequestMessage.Headers.Add("x-api-key", this._apiKey); + } + else + if (this._bearerTokenProvider is not null && !httpRequestMessage.Headers.Contains("Authentication") && await this._bearerTokenProvider().ConfigureAwait(false) is { } bearerKey) { - await this._customRequestHandler(httpRequestMessage).ConfigureAwait(false); + httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerKey); } return httpRequestMessage; } - private void Log(LogLevel logLevel, string? message, params object[] args) + 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); + + 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)) { @@ -188,4 +426,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/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 3f65e8ca2e95..cec43a1531b9 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicRequest.cs @@ -3,14 +3,17 @@ 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; 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. @@ -22,11 +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!; - - [JsonPropertyName("tools")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IList? Tools { get; set; } + public IList Messages { get; set; } = []; [JsonPropertyName("model")] public string ModelId { get; set; } = null!; @@ -35,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")] @@ -80,18 +79,15 @@ 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()); - } + [JsonConstructor] + internal AnthropicRequest() { } public void AddChatMessage(ChatMessageContent message) { Verify.NotNull(this.Messages); Verify.NotNull(message); - this.Messages.Add(CreateClaudeMessageFromChatMessage(message)); + this.Messages.Add(CreateAnthropicMessageFromChatMessage(message)); } /// @@ -107,19 +103,19 @@ 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) - => request.Messages = chatHistory.Select(CreateClaudeMessageFromChatMessage).ToList(); + private static void AddMessages(IEnumerable chatHistory, AnthropicRequest request) + => request.Messages.AddRange(chatHistory.Select(CreateAnthropicMessageFromChatMessage)); - private static Message CreateClaudeMessageFromChatMessage(ChatMessageContent message) + private static Message CreateAnthropicMessageFromChatMessage(ChatMessageContent message) { return new Message { Role = message.Role, - Contents = CreateClaudeMessages(message) + Contents = CreateAnthropicMessages(message) }; } @@ -129,7 +125,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, @@ -139,60 +139,44 @@ private static AnthropicRequest CreateRequest(ChatHistory chatHistory, Anthropic return request; } - private static List CreateClaudeMessages(ChatMessageContent content) + private static List CreateAnthropicMessages(ChatMessageContent content) { - List messages = new(); - 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; - } - - if (messages.Count == 0) - { - messages.Add(new AnthropicTextContent(content.Content ?? string.Empty)); - } - - return messages; + 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(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 AnthropicContent("text") { Text = textContent.Text ?? string.Empty }, + ImageContent imageContent => CreateAnthropicImageContent(imageContent), _ => throw new NotSupportedException($"Content type '{content.GetType().Name}' is not supported.") }; + private static AnthropicContent 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 AnthropicContent("image") + { + 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 new file mode 100644 index 000000000000..0c21e18de0cb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicResponse.cs @@ -0,0 +1,71 @@ +// 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; + +/// +/// 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? StopReason { 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/AnthropicToolFunctionDeclaration.cs b/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/AnthropicToolFunctionDeclaration.cs deleted file mode 100644 index abfbbad17779..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 of 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 c27931519b16..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,12 +4,52 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic.Core; -/// -/// Represents the request/response content of Claude. -/// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(AnthropicTextContent), typeDiscriminator: "text")] -[JsonDerivedType(typeof(AnthropicImageContent), typeDiscriminator: "image")] -[JsonDerivedType(typeof(AnthropicToolCallContent), typeDiscriminator: "tool_use")] -[JsonDerivedType(typeof(AnthropicToolResultContent), typeDiscriminator: "tool_result")] -internal abstract class AnthropicContent { } +internal sealed class AnthropicContent +{ + /// + /// 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 8dd517267cdf..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicImageContent.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -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. - /// - [JsonPropertyName("source")] - public SourceEntity Source { get; set; } - - 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. - /// - [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 ca565be761f6..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Core/Models/Message/AnthropicTextContent.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -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. - /// - [JsonPropertyName("text")] - public string Text { get; set; } -} 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/Extensions/AnthropicKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs index f5258d9b630f..dbd70a2ca5db 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicKernelBuilderExtensions.cs @@ -20,31 +20,30 @@ 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. + /// Model identifier. + /// API key. /// Optional options for the anthropic client - /// The optional service ID. /// The optional custom HttpClient. + /// Service identifier. /// The updated kernel builder. public static IKernelBuilder AddAnthropicChatCompletion( this IKernelBuilder builder, string modelId, string apiKey, AnthropicClientOptions? options = null, - string? serviceId = null, - HttpClient? httpClient = null) + HttpClient? httpClient = null, + string? serviceId = null) { Verify.NotNull(builder); - Verify.NotNull(modelId); - Verify.NotNull(apiKey); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( modelId: modelId, apiKey: apiKey, - options: options, + options: options ?? new AnthropicClientOptions(), httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), loggerFactory: serviceProvider.GetService())); + return builder; } @@ -52,34 +51,31 @@ public static IKernelBuilder AddAnthropicChatCompletion( /// 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 + /// Model identifier. + /// Bearer token provider. + /// Vertex AI Anthropic endpoint. /// Optional options for the anthropic client - /// The optional service ID. - /// The optional custom HttpClient. + /// Service identifier. /// The updated kernel builder. - public static IKernelBuilder AddAnthropicChatCompletion( + public static IKernelBuilder AddAnthropicVertextAIChatCompletion( this IKernelBuilder builder, string modelId, - Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options = null, - string? serviceId = null, - HttpClient? httpClient = null) + Func> bearerTokenProvider, + Uri? endpoint = null, + VertexAIAnthropicClientOptions? options = null, + string? serviceId = null) { Verify.NotNull(builder); - Verify.NotNull(modelId); - Verify.NotNull(endpoint); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( modelId: modelId, + bearerTokenProvider: bearerTokenProvider, + options: options ?? new VertexAIAnthropicClientOptions(), endpoint: endpoint, - requestHandler: requestHandler, - options: options, - httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + 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 9e92b2ea8857..83ed98bfafcf 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Extensions/AnthropicServiceCollectionExtensions.cs @@ -1,7 +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; @@ -17,13 +16,13 @@ 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 model for chat completion. - /// The API key for authentication Claude API. + /// The target service collection. + /// Model identifier. + /// API key. /// Optional options for the anthropic client - /// Optional service ID. + /// Service identifier. /// The updated service collection. public static IServiceCollection AddAnthropicChatCompletion( this IServiceCollection services, @@ -33,8 +32,6 @@ public static IServiceCollection AddAnthropicChatCompletion( string? serviceId = null) { Verify.NotNull(services); - Verify.NotNull(modelId); - Verify.NotNull(apiKey); services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( @@ -43,39 +40,39 @@ public static IServiceCollection AddAnthropicChatCompletion( options: options, httpClient: HttpClientProvider.GetHttpClient(serviceProvider), loggerFactory: serviceProvider.GetService())); + return services; } /// - /// 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 model for chat completion. - /// Endpoint for the chat completion model - /// A custom request handler to be used for sending HTTP requests + /// The target service collection. + /// Model identifier. + /// Bearer token provider. + /// Vertex AI Anthropic endpoint. /// Optional options for the anthropic client - /// Optional service ID. + /// Service identifier. /// The updated service collection. - public static IServiceCollection AddAnthropicChatCompletion( + public static IServiceCollection AddAnthropicVertexAIChatCompletion( this IServiceCollection services, string modelId, - Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options = null, + Func> bearerTokenProvider, + Uri? endpoint = null, + VertexAIAnthropicClientOptions? options = null, string? serviceId = null) { Verify.NotNull(services); - Verify.NotNull(modelId); - Verify.NotNull(endpoint); services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new AnthropicChatCompletionService( modelId: modelId, + bearerTokenProvider: bearerTokenProvider, endpoint: endpoint, - requestHandler: requestHandler, - options: options, + 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/AnthropicChatMessageContent.cs deleted file mode 100644 index f0a291226bef..000000000000 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicChatMessageContent.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.Anthropic.Core; - -namespace Microsoft.SemanticKernel.Connectors.Anthropic; - -/// -/// Claude specialized chat message content -/// -public sealed class AnthropicChatMessageContent : ChatMessageContent -{ - /// - /// Initializes 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; - } - - /// - /// 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 - /// The result of tool called by the kernel. - /// Additional metadata - internal AnthropicChatMessageContent( - AuthorRole role, - string? content, - string modelId, - AnthropicFunctionToolResult? calledToolResult = null, - AnthropicMetadata? metadata = null) - : base( - role: role, - content: content, - modelId: modelId, - innerContent: content, - 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; } - - /// - /// The metadata associated with the content. - /// - public new AnthropicMetadata? Metadata => (AnthropicMetadata?)base.Metadata; -} 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 55ad7872a423..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 Gemini'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. -/// -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 Gemini. -/// -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 Gemini 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 Gemini 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 Gemini 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 7ed158020e35..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 Gemini 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/Models/Contents/AnthropicChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicChatMessageContent.cs new file mode 100644 index 000000000000..4f70b5879d83 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicChatMessageContent.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Anthropic; + +/// +/// Anthropic specialized chat message content +/// +public sealed class AnthropicChatMessageContent : ChatMessageContent +{ + /// + /// Creates a new instance of the class + /// + [JsonConstructor] + internal AnthropicChatMessageContent() { } + + /// + /// The metadata associated with the content. + /// + public new AnthropicMetadata? Metadata + { + get => base.Metadata as AnthropicMetadata; + init => base.Metadata = value; + } +} diff --git a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicFinishReason.cs similarity index 89% rename from dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicFinishReason.cs index d05f9bc69547..ae1313d95663 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicFinishReason.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicFinishReason.cs @@ -7,9 +7,9 @@ namespace Microsoft.SemanticKernel.Connectors.Anthropic; /// -/// Represents a Claude Finish Reason. +/// Represents a Anthropic Finish Reason. /// -[JsonConverter(typeof(ClaudeFinishReasonConverter))] +[JsonConverter(typeof(AnthropicFinishReasonConverter))] 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. @@ -34,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) @@ -77,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/Contents/AnthropicMetadata.cs similarity index 82% rename from dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicMetadata.cs index 3cc73f27b658..c7786537ddd0 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Models/AnthropicMetadata.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/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 { @@ -46,21 +46,27 @@ 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)); } + /// + /// 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/Contents/AnthropicUsage.cs b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/AnthropicUsage.cs new file mode 100644 index 000000000000..54a2f9db3853 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Contents/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 Anthropic. +///
+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; } +} 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 70% rename from dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.Anthropic/Models/Settings/AnthropicPromptExecutionSettings.cs index 1b5b8713d5e5..e1af01ef5865 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/AnthropicPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Models/Settings/AnthropicPromptExecutionSettings.cs @@ -5,13 +5,12 @@ 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; /// -/// 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 @@ -21,7 +20,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 +101,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 +129,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/Services/AnthropicChatCompletionService.cs b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs index 0f94fafc82e1..ac52bde8aeaf 100644 --- a/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.Anthropic/Services/AnthropicChatCompletionService.cs @@ -9,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; @@ -18,15 +17,17 @@ 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. /// - /// The model for the chat completion service. - /// The API key for authentication. - /// Optional options for the anthropic client + /// 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( @@ -36,55 +37,40 @@ public AnthropicChatCompletionService( 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, + options: options ?? new AnthropicClientOptions(), + httpClient: HttpClientProvider.GetHttpClient(httpClient), 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 - /// Optional options for the anthropic client + /// 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, - Uri endpoint, - Func? requestHandler, - AnthropicClientOptions? options = null, + Func> bearerTokenProvider, + ClientOptions options, + Uri? endpoint = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNull(endpoint); - this._client = new AnthropicClient( -#pragma warning disable CA2000 - httpClient: HttpClientProvider.GetHttpClient(httpClient), -#pragma warning restore CA2000 modelId: modelId, - endpoint: endpoint, - requestHandler: requestHandler, + bearerTokenProvider: bearerTokenProvider, options: options, + endpoint: endpoint, + httpClient: HttpClientProvider.GetHttpClient(httpClient), logger: loggerFactory?.CreateLogger(typeof(AnthropicChatCompletionService))); - this._attributesInternal.Add(AIServiceExtensions.ModelIdKey, modelId); } - /// - public IReadOnlyDictionary Attributes => this._attributesInternal; - /// public Task> GetChatMessageContentsAsync( ChatHistory chatHistory, diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs new file mode 100644 index 000000000000..6e791d7aa5f9 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/AnthropicChatCompletionTests.cs @@ -0,0 +1,378 @@ +// 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) : 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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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.")] + [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 + 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); + } + + [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)); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs new file mode 100644 index 000000000000..963b719503ee --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Anthropic/TestBase.cs @@ -0,0 +1,46 @@ +// 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; +using Xunit.Abstractions; + +namespace SemanticKernel.IntegrationTests.Connectors.Anthropic; + +public abstract class TestBase(ITestOutputHelper output) +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .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; + + protected IChatCompletionService GetChatService(ServiceType serviceType) => serviceType switch + { + 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) + }; + + public enum ServiceType + { + Anthropic, + VertexAI, + AmazonBedrock + } + + 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 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 Uri AmazonBedrockGetEndpoint() => new(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 5732a3e4719a..615bb29f0dc8 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(); 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 @@ +