mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add Anthropic Agent Package (#2359)
* WIP * WIP * Simple call working * Update Thinking sample * Non-Streaming Function calling working * Update Anthropic Impl * Public Preps * UT + IT working * Update documentation + samples * Update variable * Revert nuget.config * Add IT for BetaService implementation * Remove polyfill + enable IT to run for netstandard 2.0 * Skipping Anthropic IT's for manual execution and avoid pipeline execution * Fix compilation error * Address error in UT * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix warning * Net 10 update * Update for NET 10, remove Anthropic.Foundry due to vulnerability * Final missing adjustments for NET 10 * Address PR comments * Remove unused code * Address feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace AnthropicChatCompletion.IntegrationTests;
|
||||
|
||||
public abstract class SkipAllChatClientRunStreaming(Func<AnthropicChatCompletionFixture> func) : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(func)
|
||||
{
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
|
||||
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
|
||||
}
|
||||
|
||||
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: true));
|
||||
|
||||
public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: true));
|
||||
|
||||
public class AnthropicChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: false));
|
||||
|
||||
public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: false));
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace AnthropicChatCompletion.IntegrationTests;
|
||||
|
||||
public abstract class SkipAllChatClientAgentRun(Func<AnthropicChatCompletionFixture> func) : ChatClientAgentRunTests<AnthropicChatCompletionFixture>(func)
|
||||
{
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
|
||||
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
|
||||
}
|
||||
|
||||
public class AnthropicBetaChatCompletionChatClientAgentRunTests()
|
||||
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: true));
|
||||
|
||||
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunTests()
|
||||
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: true));
|
||||
|
||||
public class AnthropicChatCompletionChatClientAgentRunTests()
|
||||
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: false));
|
||||
|
||||
public class AnthropicChatCompletionChatClientAgentReasoningRunTests()
|
||||
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: false));
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Anthropic;
|
||||
using Anthropic.Models.Beta.Messages;
|
||||
using Anthropic.Models.Messages;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace AnthropicChatCompletion.IntegrationTests;
|
||||
|
||||
public class AnthropicChatCompletionFixture : IChatClientAgentFixture
|
||||
{
|
||||
// All tests for Anthropic are intended to be ran locally as the CI pipeline for Anthropic is not setup.
|
||||
internal const string SkipReason = "Integrations tests for local execution only";
|
||||
|
||||
private static readonly AnthropicConfiguration s_config = TestConfiguration.LoadSection<AnthropicConfiguration>();
|
||||
private readonly bool _useReasoningModel;
|
||||
private readonly bool _useBeta;
|
||||
|
||||
private ChatClientAgent _agent = null!;
|
||||
|
||||
public AnthropicChatCompletionFixture(bool useReasoningChatModel, bool useBeta)
|
||||
{
|
||||
this._useReasoningModel = useReasoningChatModel;
|
||||
this._useBeta = useBeta;
|
||||
}
|
||||
|
||||
public AIAgent Agent => this._agent;
|
||||
|
||||
public IChatClient ChatClient => this._agent.ChatClient;
|
||||
|
||||
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
|
||||
{
|
||||
var typedThread = (ChatClientAgentThread)thread;
|
||||
|
||||
return typedThread.MessageStore is null ? [] : (await typedThread.MessageStore.GetMessagesAsync()).ToList();
|
||||
}
|
||||
|
||||
public Task<ChatClientAgent> CreateChatClientAgentAsync(
|
||||
string name = "HelpfulAssistant",
|
||||
string instructions = "You are a helpful assistant.",
|
||||
IList<AITool>? aiTools = null)
|
||||
{
|
||||
var anthropicClient = new AnthropicClient() { APIKey = s_config.ApiKey };
|
||||
|
||||
IChatClient? chatClient = this._useBeta
|
||||
? anthropicClient
|
||||
.Beta
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(options
|
||||
=> options.RawRepresentationFactory = _
|
||||
=> new Anthropic.Models.Beta.Messages.MessageCreateParams()
|
||||
{
|
||||
Model = options.ModelId ?? (this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId),
|
||||
MaxTokens = options.MaxOutputTokens ?? 4096,
|
||||
Messages = [],
|
||||
Thinking = this._useReasoningModel
|
||||
? new BetaThinkingConfigParam(new BetaThinkingConfigEnabled(2048))
|
||||
: new BetaThinkingConfigParam(new BetaThinkingConfigDisabled())
|
||||
}).Build()
|
||||
|
||||
: anthropicClient
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(options
|
||||
=> options.RawRepresentationFactory = _
|
||||
=> new Anthropic.Models.Messages.MessageCreateParams()
|
||||
{
|
||||
Model = options.ModelId ?? (this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId),
|
||||
MaxTokens = options.MaxOutputTokens ?? 4096,
|
||||
Messages = [],
|
||||
Thinking = this._useReasoningModel
|
||||
? new ThinkingConfigParam(new ThinkingConfigEnabled(2048))
|
||||
: new ThinkingConfigParam(new ThinkingConfigDisabled())
|
||||
}).Build();
|
||||
|
||||
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
|
||||
{
|
||||
Name = name,
|
||||
Instructions = instructions,
|
||||
ChatOptions = new() { Tools = aiTools }
|
||||
}));
|
||||
}
|
||||
|
||||
public Task DeleteAgentAsync(ChatClientAgent agent) =>
|
||||
// Chat Completion does not require/support deleting agents, so this is a no-op.
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task DeleteThreadAsync(AgentThread thread) =>
|
||||
// Chat Completion does not require/support deleting threads, so this is a no-op.
|
||||
Task.CompletedTask;
|
||||
|
||||
public async Task InitializeAsync() =>
|
||||
this._agent = await this.CreateChatClientAgentAsync();
|
||||
|
||||
public Task DisposeAsync() =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace AnthropicChatCompletion.IntegrationTests;
|
||||
|
||||
public abstract class SkipAllRunStreaming(Func<AnthropicChatCompletionFixture> func) : RunStreamingTests<AnthropicChatCompletionFixture>(func)
|
||||
{
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync();
|
||||
}
|
||||
|
||||
public class AnthropicBetaChatCompletionRunStreamingTests()
|
||||
: SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: true));
|
||||
|
||||
public class AnthropicBetaChatCompletionReasoningRunStreamingTests()
|
||||
: SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: true));
|
||||
|
||||
public class AnthropicChatCompletionRunStreamingTests()
|
||||
: SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: false));
|
||||
|
||||
public class AnthropicChatCompletionReasoningRunStreamingTests()
|
||||
: SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: false));
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace AnthropicChatCompletion.IntegrationTests;
|
||||
|
||||
public abstract class SkipAllRun(Func<AnthropicChatCompletionFixture> func) : RunTests<AnthropicChatCompletionFixture>(func)
|
||||
{
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync();
|
||||
|
||||
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
|
||||
public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync();
|
||||
}
|
||||
|
||||
public class AnthropicBetaChatCompletionRunTests()
|
||||
: SkipAllRun(() => new(useReasoningChatModel: false, useBeta: true));
|
||||
|
||||
public class AnthropicBetaChatCompletionReasoningRunTests()
|
||||
: SkipAllRun(() => new(useReasoningChatModel: true, useBeta: true));
|
||||
|
||||
public class AnthropicChatCompletionRunTests()
|
||||
: SkipAllRun(() => new(useReasoningChatModel: false, useBeta: false));
|
||||
|
||||
public class AnthropicChatCompletionReasoningRunTests()
|
||||
: SkipAllRun(() => new(useReasoningChatModel: true, useBeta: false));
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0052 // Remove unread private members
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Anthropic.Services;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using IBetaMessageService = Anthropic.Services.Beta.IMessageService;
|
||||
using IMessageService = Anthropic.Services.IMessageService;
|
||||
|
||||
namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the AnthropicClientExtensions class.
|
||||
/// </summary>
|
||||
public sealed class AnthropicBetaServiceExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
var testChatClient = new TestChatClient(chatClient.Beta.AsIChatClient());
|
||||
|
||||
// Act
|
||||
var agent = chatClient.Beta.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
name: "Test Agent",
|
||||
description: "Test description",
|
||||
clientFactory: (innerClient) => testChatClient);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
Assert.Equal("Test description", agent.Description);
|
||||
|
||||
// Verify that the custom chat client can be retrieved from the agent's service collection
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.NotNull(retrievedTestClient);
|
||||
Assert.Same(testChatClient, retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithClientFactoryUsingAsBuilder_AppliesFactoryCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
TestChatClient? testChatClient = null;
|
||||
|
||||
// Act
|
||||
var agent = chatClient.Beta.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
clientFactory: (innerClient) =>
|
||||
innerClient.AsBuilder().Use((innerClient) => testChatClient = new TestChatClient(innerClient)).Build());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
|
||||
// Verify that the custom chat client can be retrieved from the agent's service collection
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.NotNull(retrievedTestClient);
|
||||
Assert.Same(testChatClient, retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
var testChatClient = new TestChatClient(chatClient.Beta.AsIChatClient());
|
||||
var options = new ChatClientAgentOptions
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "Test description",
|
||||
Instructions = "Test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
var agent = chatClient.Beta.CreateAIAgent(
|
||||
options,
|
||||
clientFactory: (innerClient) => testChatClient);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
Assert.Equal("Test description", agent.Description);
|
||||
|
||||
// Verify that the custom chat client can be retrieved from the agent's service collection
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.NotNull(retrievedTestClient);
|
||||
Assert.Same(testChatClient, retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent without clientFactory works normally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithoutClientFactory_WorksNormally()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
|
||||
// Act
|
||||
var agent = chatClient.Beta.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
name: "Test Agent");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
|
||||
// Verify that no TestChatClient is available since no factory was provided
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.Null(retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with null clientFactory works normally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullClientFactory_WorksNormally()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
|
||||
// Act
|
||||
var agent = chatClient.Beta.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
name: "Test Agent",
|
||||
clientFactory: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
|
||||
// Verify that no TestChatClient is available since no factory was provided
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.Null(retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent throws ArgumentNullException when client is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
((IBetaService)null!).CreateAIAgent("test-model"));
|
||||
|
||||
Assert.Equal("betaService", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with options throws ArgumentNullException when options is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
chatClient.Beta.CreateAIAgent((ChatClientAgentOptions)null!));
|
||||
|
||||
Assert.Equal("options", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test custom chat client that can be used to verify clientFactory functionality.
|
||||
/// </summary>
|
||||
private sealed class TestChatClient : IChatClient
|
||||
{
|
||||
private readonly IChatClient _innerClient;
|
||||
|
||||
public TestChatClient(IChatClient innerClient)
|
||||
{
|
||||
this._innerClient = innerClient;
|
||||
}
|
||||
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> this._innerClient.GetResponseAsync(messages, options, cancellationToken);
|
||||
|
||||
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await foreach (var update in this._innerClient.GetStreamingResponseAsync(messages, options, cancellationToken))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
// Return this instance when requested
|
||||
if (serviceType == typeof(TestChatClient))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return this._innerClient.GetService(serviceType, serviceKey);
|
||||
}
|
||||
|
||||
public void Dispose() => this._innerClient.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test ChatClient implementation for testing.
|
||||
/// </summary>
|
||||
private sealed class TestAnthropicChatClient : IAnthropicClient
|
||||
{
|
||||
public TestAnthropicChatClient()
|
||||
{
|
||||
this.BetaService = new TestBetaService(this);
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public Uri BaseUrl { get => new("http://localhost"); init => throw new NotImplementedException(); }
|
||||
public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public string? APIKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
|
||||
public IMessageService Messages => throw new NotImplementedException();
|
||||
|
||||
public IModelService Models => throw new NotImplementedException();
|
||||
|
||||
public IBetaService Beta => this.BetaService;
|
||||
|
||||
public IBetaService BetaService { get; }
|
||||
|
||||
IMessageService IAnthropicClient.Messages => new Mock<IMessageService>().Object;
|
||||
|
||||
public Task<HttpResponse> Execute<T>(HttpRequest<T> request, CancellationToken cancellationToken = default) where T : ParamsBase
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IAnthropicClient WithOptions(Func<ClientOptions, ClientOptions> modifier)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private sealed class TestBetaService : IBetaService
|
||||
{
|
||||
private readonly IAnthropicClient _client;
|
||||
|
||||
public TestBetaService(IAnthropicClient client)
|
||||
{
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
public global::Anthropic.Services.Beta.IModelService Models => throw new NotImplementedException();
|
||||
|
||||
public global::Anthropic.Services.Beta.IFileService Files => throw new NotImplementedException();
|
||||
|
||||
public global::Anthropic.Services.Beta.ISkillService Skills => throw new NotImplementedException();
|
||||
|
||||
public IBetaMessageService Messages => new Mock<IBetaMessageService>().Object;
|
||||
|
||||
public IBetaService WithOptions(Func<ClientOptions, ClientOptions> modifier)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Anthropic.Services;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the AnthropicClientExtensions class.
|
||||
/// </summary>
|
||||
public sealed class AnthropicClientExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Test custom chat client that can be used to verify clientFactory functionality.
|
||||
/// </summary>
|
||||
private sealed class TestChatClient : IChatClient
|
||||
{
|
||||
private readonly IChatClient _innerClient;
|
||||
|
||||
public TestChatClient(IChatClient innerClient)
|
||||
{
|
||||
this._innerClient = innerClient;
|
||||
}
|
||||
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> this._innerClient.GetResponseAsync(messages, options, cancellationToken);
|
||||
|
||||
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await foreach (var update in this._innerClient.GetStreamingResponseAsync(messages, options, cancellationToken))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
// Return this instance when requested
|
||||
if (serviceType == typeof(TestChatClient))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return this._innerClient.GetService(serviceType, serviceKey);
|
||||
}
|
||||
|
||||
public void Dispose() => this._innerClient.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test ChatClient implementation for testing.
|
||||
/// </summary>
|
||||
private sealed class TestAnthropicChatClient : IAnthropicClient
|
||||
{
|
||||
public TestAnthropicChatClient()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public Uri BaseUrl { get => new("http://localhost"); init => throw new NotImplementedException(); }
|
||||
public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public string? APIKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
|
||||
|
||||
public IMessageService Messages => throw new NotImplementedException();
|
||||
|
||||
public IModelService Models => throw new NotImplementedException();
|
||||
|
||||
public IBetaService Beta => throw new NotImplementedException();
|
||||
|
||||
public Task<HttpResponse> Execute<T>(HttpRequest<T> request, CancellationToken cancellationToken = default) where T : ParamsBase
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IAnthropicClient WithOptions(Func<ClientOptions, ClientOptions> modifier)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
var testChatClient = new TestChatClient(chatClient.AsIChatClient());
|
||||
|
||||
// Act
|
||||
var agent = chatClient.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
name: "Test Agent",
|
||||
description: "Test description",
|
||||
clientFactory: (innerClient) => testChatClient);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
Assert.Equal("Test description", agent.Description);
|
||||
|
||||
// Verify that the custom chat client can be retrieved from the agent's service collection
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.NotNull(retrievedTestClient);
|
||||
Assert.Same(testChatClient, retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithClientFactoryUsingAsBuilder_AppliesFactoryCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
TestChatClient? testChatClient = null;
|
||||
|
||||
// Act
|
||||
var agent = chatClient.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
clientFactory: (innerClient) =>
|
||||
innerClient.AsBuilder().Use((innerClient) => testChatClient = new TestChatClient(innerClient)).Build());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
|
||||
// Verify that the custom chat client can be retrieved from the agent's service collection
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.NotNull(retrievedTestClient);
|
||||
Assert.Same(testChatClient, retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
var testChatClient = new TestChatClient(chatClient.AsIChatClient());
|
||||
var options = new ChatClientAgentOptions
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "Test description",
|
||||
Instructions = "Test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
var agent = chatClient.CreateAIAgent(
|
||||
options,
|
||||
clientFactory: (innerClient) => testChatClient);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
Assert.Equal("Test description", agent.Description);
|
||||
|
||||
// Verify that the custom chat client can be retrieved from the agent's service collection
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.NotNull(retrievedTestClient);
|
||||
Assert.Same(testChatClient, retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent without clientFactory works normally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithoutClientFactory_WorksNormally()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
|
||||
// Act
|
||||
var agent = chatClient.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
name: "Test Agent");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
|
||||
// Verify that no TestChatClient is available since no factory was provided
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.Null(retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with null clientFactory works normally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullClientFactory_WorksNormally()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
|
||||
// Act
|
||||
var agent = chatClient.CreateAIAgent(
|
||||
model: "test-model",
|
||||
instructions: "Test instructions",
|
||||
name: "Test Agent",
|
||||
clientFactory: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal("Test Agent", agent.Name);
|
||||
|
||||
// Verify that no TestChatClient is available since no factory was provided
|
||||
var retrievedTestClient = agent.GetService<TestChatClient>();
|
||||
Assert.Null(retrievedTestClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent throws ArgumentNullException when client is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
((TestAnthropicChatClient)null!).CreateAIAgent("test-model"));
|
||||
|
||||
Assert.Equal("client", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that CreateAIAgent with options throws ArgumentNullException when options is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new TestAnthropicChatClient();
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
chatClient.CreateAIAgent((ChatClientAgentOptions)null!));
|
||||
|
||||
Assert.Equal("options", exception.ParamName);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user