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 @@
+