diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index a49f0fc277..62793f39fd 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -11,6 +11,8 @@ + + @@ -169,4 +171,4 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 7f4a6eb4d1..f1efae7ba8 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -45,6 +45,7 @@ + @@ -82,6 +83,12 @@ + + + + + + @@ -312,6 +319,7 @@ + @@ -336,6 +344,7 @@ + @@ -358,6 +367,7 @@ + @@ -374,6 +384,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj new file mode 100644 index 0000000000..002bd066fe --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + + enable + enable + $(NoWarn);IDE0059 + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs new file mode 100644 index 0000000000..531b56e1a1 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use an AI agent with Anthropic as the backend. + +using System.ClientModel; +using System.Net.Http.Headers; +using Anthropic; +using Anthropic.Core; +using Azure.Core; +using Azure.Identity; +using Microsoft.Agents.AI; +using Sample; + +var deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_DEPLOYMENT_NAME") ?? "claude-haiku-4-5"; + +// The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri +// ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions +var resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE"); +var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"); + +const string JokerInstructions = "You are good at telling jokes."; +const string JokerName = "JokerAgent"; + +AnthropicClient? client = (resource is null) + ? new AnthropicClient() { APIKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API + : (apiKey is not null) + ? new AnthropicFoundryClient(resource, new ApiKeyCredential(apiKey)) // If an apiKey is provided, use Foundry with ApiKey authentication + : new AnthropicFoundryClient(resource, new AzureCliCredential()); // Otherwise, use Foundry with Azure Client authentication + +AIAgent agent = client.CreateAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); + +namespace Sample +{ + /// + /// Provides methods for invoking the Azure hosted Anthropic api. + /// + public class AnthropicFoundryClient : AnthropicClient + { + private readonly TokenCredential _tokenCredential; + private readonly string _resourceName; + + /// + /// Creates a new instance of the . + /// + /// The service resource subdomain name to use in the anthropic azure endpoint + /// The credential provider. Use any specialization of to get your access token in supported environments. + /// Set of client option configurations + /// Resource is null + /// TokenCredential is null + /// + /// Any APIKey or Bearer token provided will be ignored in favor of the provided in the constructor + /// + public AnthropicFoundryClient(string resourceName, TokenCredential tokenCredential, Anthropic.Core.ClientOptions? options = null) : base(options ?? new()) + { + this._resourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName)); + this._tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential)); + this.BaseUrl = new Uri($"https://{this._resourceName}.services.ai.azure.com/anthropic", UriKind.Absolute); + } + + /// + /// Creates a new instance of the . + /// + /// The service resource subdomain name to use in the anthropic azure endpoint + /// The api key. + /// Set of client option configurations + /// Resource is null + /// Api key is null + /// + /// Any APIKey or Bearer token provided will be ignored in favor of the provided in the constructor + /// + public AnthropicFoundryClient(string resourceName, ApiKeyCredential apiKeyCredential, Anthropic.Core.ClientOptions? options = null) : + this(resourceName, apiKeyCredential is null + ? throw new ArgumentNullException(nameof(apiKeyCredential)) + : DelegatedTokenCredential.Create((_, _) => + { + apiKeyCredential.Deconstruct(out string dangerousCredential); + return new AccessToken(dangerousCredential, DateTimeOffset.MaxValue); + }), + options) + { } + + public override IAnthropicClient WithOptions(Func modifier) + => this; + + protected override ValueTask BeforeSend( + HttpRequest request, + HttpRequestMessage requestMessage, + CancellationToken cancellationToken + ) + { + var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken); + + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token); + + return default; + } + } +} diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md new file mode 100644 index 0000000000..afcf391572 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md @@ -0,0 +1,53 @@ +# Creating an AIAgent with Anthropic + +This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service. + +The sample supports three deployment scenarios: + +1. **Anthropic Public API** - Direct connection to Anthropic's public API +2. **Azure Foundry with API Key** - Anthropic models deployed through Azure Foundry using API key authentication +3. **Azure Foundry with Azure CLI** - Anthropic models deployed through Azure Foundry using Azure CLI credentials + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later + +### For Anthropic Public API + +- Anthropic API key + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key +$env:ANTHROPIC_DEPLOYMENT_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5 +``` + +### For Azure Foundry with API Key + +- Azure Foundry service endpoint and deployment configured +- Anthropic API key + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure Foundry resource name (subdomain before .services.ai.azure.com) +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key +$env:ANTHROPIC_DEPLOYMENT_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5 +``` + +### For Azure Foundry with Azure CLI + +- Azure Foundry service endpoint and deployment configured +- Azure CLI installed and authenticated (for Azure credential authentication) + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure Foundry resource name (subdomain before .services.ai.azure.com) +$env:ANTHROPIC_DEPLOYMENT_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5 +``` + +**Note**: When using Azure Foundry with Azure CLI, make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively). diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj index eeda3eef6f..4ea7a45b8a 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj @@ -2,7 +2,7 @@ Exe - net10.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs index 9b03c989e1..331109fba9 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs @@ -3,6 +3,7 @@ // This sample shows how to create and use a simple AI agent with OpenAI Chat Completion as the backend. using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; using OpenAI; var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set."); diff --git a/dotnet/samples/GettingStarted/AgentProviders/README.md b/dotnet/samples/GettingStarted/AgentProviders/README.md index 5d32f2542b..964e560c9a 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/README.md @@ -15,6 +15,7 @@ See the README.md for each sample for the prerequisites for that sample. |Sample|Description| |---|---| |[Creating an AIAgent with A2A](./Agent_With_A2A/)|This sample demonstrates how to create AIAgent for an existing A2A agent.| +|[Creating an AIAgent with Anthropic](./Agent_With_Anthropic/)|This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service| |[Creating an AIAgent with Foundry Agents using Azure.AI.Agents.Persistent](./Agent_With_AzureAIAgentsPersistent/)|This sample demonstrates how to create a Foundry Persistent agent and expose it as an AIAgent using the Azure.AI.Agents.Persistent SDK| |[Creating an AIAgent with Foundry Agents using Azure.AI.Project](./Agent_With_AzureAIProject/)|This sample demonstrates how to create an Foundry Project agent and expose it as an AIAgent using the Azure.AI.Project SDK| |[Creating an AIAgent with AzureFoundry Model](./Agent_With_AzureFoundryModel/)|This sample demonstrates how to use any model deployed to Azure Foundry to create an AIAgent| diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj new file mode 100644 index 0000000000..09359c5e78 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs new file mode 100644 index 0000000000..cf7e29c2fe --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with Anthropic as the backend. + +using Anthropic; +using Anthropic.Core; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); +var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5"; + +AIAgent agent = new AnthropicClient(new ClientOptions { APIKey = apiKey }) + .CreateAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker"); + +// Invoke the agent and output the text result. +var response = await agent.RunAsync("Tell me a joke about a pirate."); +Console.WriteLine(response); + +// Invoke the agent with streaming support. +await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.")) +{ + Console.WriteLine(update); +} diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md new file mode 100644 index 0000000000..4800650bd9 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md @@ -0,0 +1,43 @@ +# Running a simple agent with Anthropic + +This sample demonstrates how to create and run a basic agent with Anthropic Claude models. + +## What this sample demonstrates + +- Creating an AI agent with Anthropic Claude +- Running a simple agent with instructions +- Managing agent lifecycle + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Anthropic API key configured + +**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/). + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key +$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model +``` + +## Run the sample + +Navigate to the AgentWithAnthropic sample directory and run: + +```powershell +cd dotnet\samples\GettingStarted\AgentWithAnthropic +dotnet run --project .\Agent_Anthropic_Step01_Running +``` + +## Expected behavior + +The sample will: + +1. Create an agent with Anthropic Claude +2. Run the agent with a simple prompt +3. Display the agent's response + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj new file mode 100644 index 0000000000..fc0914f1fc --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs new file mode 100644 index 0000000000..d362a9dd0d --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use an AI agent with reasoning capabilities. + +using Anthropic; +using Anthropic.Core; +using Anthropic.Models.Messages; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); +var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5"; +var maxTokens = 4096; +var thinkingTokens = 2048; + +var agent = new AnthropicClient(new ClientOptions { APIKey = apiKey }) + .CreateAIAgent( + model: model, + clientFactory: (chatClient) => chatClient + .AsBuilder() + .ConfigureOptions( + options => options.RawRepresentationFactory = (_) => new MessageCreateParams() + { + Model = options.ModelId ?? model, + MaxTokens = options.MaxOutputTokens ?? maxTokens, + Messages = [], + Thinking = new ThinkingConfigParam(new ThinkingConfigEnabled(budgetTokens: thinkingTokens)) + }) + .Build()); + +Console.WriteLine("1. Non-streaming:"); +var response = await agent.RunAsync("Solve this problem step by step: If a train travels 60 miles per hour and needs to cover 180 miles, how long will the journey take? Show your reasoning."); + +Console.WriteLine("#### Start Thinking ####"); +Console.WriteLine($"\e[92m{string.Join("\n", response.Messages.SelectMany(m => m.Contents.OfType().Select(c => c.Text)))}\e[0m"); +Console.WriteLine("#### End Thinking ####"); + +Console.WriteLine("\n#### Final Answer ####"); +Console.WriteLine(response.Text); + +Console.WriteLine("Token usage:"); +Console.WriteLine($"Input: {response.Usage?.InputTokenCount}, Output: {response.Usage?.OutputTokenCount}, {string.Join(", ", response.Usage?.AdditionalCounts ?? [])}"); +Console.WriteLine(); + +Console.WriteLine("2. Streaming"); +await foreach (var update in agent.RunStreamingAsync("Explain the theory of relativity in simple terms.")) +{ + foreach (var item in update.Contents) + { + if (item is TextReasoningContent reasoningContent) + { + Console.WriteLine($"\e[92m{reasoningContent.Text}\e[0m"); + } + else if (item is TextContent textContent) + { + Console.WriteLine(textContent.Text); + } + } +} diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md new file mode 100644 index 0000000000..ae088b2386 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md @@ -0,0 +1,46 @@ +# Using reasoning with Anthropic agents + +This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents. + +## What this sample demonstrates + +- Creating an AI agent with Anthropic Claude extended thinking +- Using reasoning capabilities for complex problem solving +- Extracting thinking and response content from agent output +- Managing agent lifecycle + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Anthropic API key configured +- Access to Anthropic Claude models with extended thinking support + +**Note**: This sample uses Anthropic Claude models with extended thinking. For more information, see [Anthropic documentation](https://docs.anthropic.com/). + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key +$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model +``` + +## Run the sample + +Navigate to the AgentWithAnthropic sample directory and run: + +```powershell +cd dotnet\samples\GettingStarted\AgentWithAnthropic +dotnet run --project .\Agent_Anthropic_Step02_Reasoning +``` + +## Expected behavior + +The sample will: + +1. Create an agent with Anthropic Claude extended thinking enabled +2. Run the agent with a complex reasoning prompt +3. Display the agent's thinking process +4. Display the agent's final response + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj new file mode 100644 index 0000000000..fdb9a2f50f --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs new file mode 100644 index 0000000000..a56db8d4a2 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use an agent with function tools. +// It shows both non-streaming and streaming agent interactions using weather-related tools. + +using System.ComponentModel; +using Anthropic; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); +var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5"; + +[Description("Get the weather for a given location.")] +static string GetWeather([Description("The location to get the weather for.")] string location) + => $"The weather in {location} is cloudy with a high of 15°C."; + +const string AssistantInstructions = "You are a helpful assistant that can get weather information."; +const string AssistantName = "WeatherAssistant"; + +// Define the agent with function tools. +AITool tool = AIFunctionFactory.Create(GetWeather); + +// Get anthropic client to create agents. +AIAgent agent = new AnthropicClient { APIKey = apiKey } + .CreateAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]); + +// Non-streaming agent interaction with function tools. +AgentThread thread = agent.GetNewThread(); +Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread)); + +// Streaming agent interaction with function tools. +thread = agent.GetNewThread(); +await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread)) +{ + Console.WriteLine(update); +} diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md new file mode 100644 index 0000000000..6c905864ef --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md @@ -0,0 +1,47 @@ +# Using Function Tools with Anthropic agents + +This sample demonstrates how to use function tools with Anthropic Claude agents, allowing agents to call custom functions to retrieve information. + +## What this sample demonstrates + +- Creating function tools using AIFunctionFactory +- Passing function tools to an Anthropic Claude agent +- Running agents with function tools (text output) +- Running agents with function tools (streaming output) +- Managing agent lifecycle + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Anthropic API key configured + +**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/). + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key +$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model +``` + +## Run the sample + +Navigate to the AgentWithAnthropic sample directory and run: + +```powershell +cd dotnet\samples\GettingStarted\AgentWithAnthropic +dotnet run --project .\Agent_Anthropic_Step03_UsingFunctionTools +``` + +## Expected behavior + +The sample will: + +1. Create an agent named "WeatherAssistant" with a GetWeather function tool +2. Run the agent with a text prompt asking about weather +3. The agent will invoke the GetWeather function tool to retrieve weather information +4. Run the agent again with streaming to display the response as it's generated +5. Clean up resources by deleting the agent + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md new file mode 100644 index 0000000000..44c15b384b --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md @@ -0,0 +1,72 @@ +# Getting started with agents using Anthropic + +The getting started with agents using Anthropic samples demonstrate the fundamental concepts and functionalities +of single agents using Anthropic as the AI provider. + +These samples use Anthropic Claude models as the AI provider and use ChatCompletion as the type of service. + +For other samples that demonstrate how to create and configure each type of agent that come with the agent framework, +see the [How to create an agent for each provider](../AgentProviders/README.md) samples. + +## Getting started with agents using Anthropic prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Anthropic API key configured +- User has access to Anthropic Claude models + +**Note**: These samples use Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/). + +## Using Anthropic with Azure Foundry + +To use Anthropic with Azure Foundry, you can check the sample [AgentProviders/Agent_With_Anthropic](../AgentProviders/Agent_With_Anthropic/README.md) for more details. + +## Samples + +|Sample|Description| +|---|---| +|[Running a simple agent](./Agent_Anthropic_Step01_Running/)|This sample demonstrates how to create and run a basic agent with Anthropic Claude| +|[Using reasoning with an agent](./Agent_Anthropic_Step02_Reasoning/)|This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents| +|[Using function tools with an agent](./Agent_Anthropic_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with an Anthropic Claude agent| + +## Running the samples from the console + +To run the samples, navigate to the desired sample directory, e.g. + +```powershell +cd Agent_Anthropic_Step01_Running +``` + +Set the following environment variables: + +```powershell +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key +``` + +If the variables are not set, you will be prompted for the values when running the samples. + +Execute the following command to build the sample: + +```powershell +dotnet build +``` + +Execute the following command to run the sample: + +```powershell +dotnet run --no-build +``` + +Or just build and run in one step: + +```powershell +dotnet run +``` + +## Running the samples from Visual Studio + +Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`. + +You will be prompted for any required environment variables if they are not already set. + diff --git a/dotnet/samples/GettingStarted/README.md b/dotnet/samples/GettingStarted/README.md index 4e349c7742..7a46d81a62 100644 --- a/dotnet/samples/GettingStarted/README.md +++ b/dotnet/samples/GettingStarted/README.md @@ -15,5 +15,6 @@ of the agent framework. |[A2A](./A2A/README.md)|Getting started with A2A (Agent-to-Agent) specific features| |[Agent Open Telemetry](./AgentOpenTelemetry/README.md)|Getting started with OpenTelemetry for agents| |[Agent With OpenAI exchange types](./AgentWithOpenAI/README.md)|Using OpenAI exchange types with agents| +|[Agent With Anthropic](./AgentWithAnthropic/README.md)|Getting started with agents using Anthropic Claude| |[Workflow](./Workflows/README.md)|Getting started with Workflow| |[Model Context Protocol](./ModelContextProtocol/README.md)|Getting started with Model Context Protocol| diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs new file mode 100644 index 0000000000..9bf698bded --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Anthropic.Services; + +/// +/// Provides extension methods for the class. +/// +public static class AnthropicBetaServiceExtensions +{ + /// + /// Specifies the default maximum number of tokens allowed for processing operations. + /// + public static int DefaultMaxTokens { get; set; } = 4096; + + /// + /// Creates a new AI agent using the specified model and options. + /// + /// The Anthropic beta service. + /// The model to use for chat completions. + /// The instructions for the AI agent. + /// The name of the AI agent. + /// The description of the AI agent. + /// The tools available to the AI agent. + /// The default maximum tokens for chat completions. Defaults to if not provided. + /// Provides a way to customize the creation of the underlying used by the agent. + /// Optional logger factory for enabling logging within the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// The created AI agent. + public static ChatClientAgent CreateAIAgent( + this IBetaService betaService, + string model, + string? instructions = null, + string? name = null, + string? description = null, + IList? tools = null, + int? defaultMaxTokens = null, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) + { + var options = new ChatClientAgentOptions + { + Instructions = instructions, + Name = name, + Description = description, + }; + + if (tools is { Count: > 0 }) + { + options.ChatOptions = new ChatOptions { Tools = tools }; + } + + var chatClient = betaService.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, options, loggerFactory, services); + } + + /// + /// Creates an AI agent from an using the Anthropic Chat Completion API. + /// + /// The Anthropic to use for the agent. + /// Full set of options to configure the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// Optional logger factory for enabling logging within the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// An instance backed by the Anthropic Chat Completion service. + /// Thrown when or is . + public static ChatClientAgent CreateAIAgent( + this IBetaService betaService, + ChatClientAgentOptions options, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(betaService); + Throw.IfNull(options); + + var chatClient = betaService.AsIChatClient(); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, options, loggerFactory, services); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs new file mode 100644 index 0000000000..f43f4bd0ce --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Anthropic; + +/// +/// Provides extension methods for the class. +/// +public static class AnthropicClientExtensions +{ + /// + /// Specifies the default maximum number of tokens allowed for processing operations. + /// + public static int DefaultMaxTokens { get; set; } = 4096; + + /// + /// Creates a new AI agent using the specified model and options. + /// + /// An Anthropic to use with the agent.. + /// The model to use for chat completions. + /// The instructions for the AI agent. + /// The name of the AI agent. + /// The description of the AI agent. + /// The tools available to the AI agent. + /// The default maximum tokens for chat completions. Defaults to if not provided. + /// Provides a way to customize the creation of the underlying used by the agent. + /// Optional logger factory for enabling logging within the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// The created AI agent. + public static ChatClientAgent CreateAIAgent( + this IAnthropicClient client, + string model, + string? instructions = null, + string? name = null, + string? description = null, + IList? tools = null, + int? defaultMaxTokens = null, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) + { + var options = new ChatClientAgentOptions + { + Instructions = instructions, + Name = name, + Description = description, + }; + + if (tools is { Count: > 0 }) + { + options.ChatOptions = new ChatOptions { Tools = tools }; + } + + var chatClient = client.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, options, loggerFactory, services); + } + + /// + /// Creates an AI agent from an using the Anthropic Chat Completion API. + /// + /// An Anthropic to use with the agent.. + /// Full set of options to configure the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// Optional logger factory for enabling logging within the agent. + /// An optional to use for resolving services required by the instances being invoked. + /// An instance backed by the Anthropic Chat Completion service. + /// Thrown when or is . + public static ChatClientAgent CreateAIAgent( + this IAnthropicClient client, + ChatClientAgentOptions options, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) + { + Throw.IfNull(client); + Throw.IfNull(options); + + var chatClient = client.AsIChatClient(); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, options, loggerFactory, services); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientJsonContext.cs new file mode 100644 index 0000000000..080745f148 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientJsonContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable CA1812 + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Anthropic; + +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(Dictionary))] +internal sealed partial class AnthropicClientJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj new file mode 100644 index 0000000000..60b90a0212 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -0,0 +1,26 @@ + + + + preview + enable + true + + + + + + + + + + + + + + + + Microsoft Agent Framework Anthropic Agents + Provides Microsoft Agent Framework support for Anthropic Agents. + + + diff --git a/dotnet/src/Shared/IntegrationTests/AnthropicConfiguration.cs b/dotnet/src/Shared/IntegrationTests/AnthropicConfiguration.cs new file mode 100644 index 0000000000..2230be95ed --- /dev/null +++ b/dotnet/src/Shared/IntegrationTests/AnthropicConfiguration.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Shared.IntegrationTests; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +#pragma warning disable CA1812 // Internal class that is apparently never instantiated. + +internal sealed class AnthropicConfiguration +{ + public string? ServiceId { get; set; } + + public string ChatModelId { get; set; } + + public string ChatReasoningModelId { get; set; } + + public string ApiKey { get; set; } +} diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj new file mode 100644 index 0000000000..929eafe998 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj @@ -0,0 +1,20 @@ + + + + True + + + + + + + + + + + + + + + + diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs new file mode 100644 index 0000000000..992db5380b --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs @@ -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 func) : ChatClientAgentRunStreamingTests(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)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs new file mode 100644 index 0000000000..e2ce6e5d04 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs @@ -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 func) : ChatClientAgentRunTests(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)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs new file mode 100644 index 0000000000..76ca18d3de --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -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(); + 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> GetChatHistoryAsync(AgentThread thread) + { + var typedThread = (ChatClientAgentThread)thread; + + return typedThread.MessageStore is null ? [] : (await typedThread.MessageStore.GetMessagesAsync()).ToList(); + } + + public Task CreateChatClientAgentAsync( + string name = "HelpfulAssistant", + string instructions = "You are a helpful assistant.", + IList? 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; +} diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs new file mode 100644 index 0000000000..f1bbbe47e9 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs @@ -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 func) : RunStreamingTests(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)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs new file mode 100644 index 0000000000..aadbf747c2 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs @@ -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 func) : RunTests(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)); diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs new file mode 100644 index 0000000000..af778c03c9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -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; + +/// +/// Unit tests for the AnthropicClientExtensions class. +/// +public sealed class AnthropicBetaServiceExtensionsTests +{ + /// + /// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory. + /// + [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(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly. + /// + [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(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory. + /// + [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(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent without clientFactory works normally. + /// + [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(); + Assert.Null(retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent with null clientFactory works normally. + /// + [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(); + Assert.Null(retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent throws ArgumentNullException when client is null. + /// + [Fact] + public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException() + { + // Act & Assert + var exception = Assert.Throws(() => + ((IBetaService)null!).CreateAIAgent("test-model")); + + Assert.Equal("betaService", exception.ParamName); + } + + /// + /// Verify that CreateAIAgent with options throws ArgumentNullException when options is null. + /// + [Fact] + public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException() + { + // Arrange + var chatClient = new TestAnthropicChatClient(); + + // Act & Assert + var exception = Assert.Throws(() => + chatClient.Beta.CreateAIAgent((ChatClientAgentOptions)null!)); + + Assert.Equal("options", exception.ParamName); + } + + /// + /// Test custom chat client that can be used to verify clientFactory functionality. + /// + private sealed class TestChatClient : IChatClient + { + private readonly IChatClient _innerClient; + + public TestChatClient(IChatClient innerClient) + { + this._innerClient = innerClient; + } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => this._innerClient.GetResponseAsync(messages, options, cancellationToken); + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable 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(); + } + + /// + /// Creates a test ChatClient implementation for testing. + /// + 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().Object; + + public Task Execute(HttpRequest request, CancellationToken cancellationToken = default) where T : ParamsBase + { + throw new NotImplementedException(); + } + + public IAnthropicClient WithOptions(Func 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().Object; + + public IBetaService WithOptions(Func modifier) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs new file mode 100644 index 0000000000..7a9c34a508 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs @@ -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; + +/// +/// Unit tests for the AnthropicClientExtensions class. +/// +public sealed class AnthropicClientExtensionsTests +{ + /// + /// Test custom chat client that can be used to verify clientFactory functionality. + /// + private sealed class TestChatClient : IChatClient + { + private readonly IChatClient _innerClient; + + public TestChatClient(IChatClient innerClient) + { + this._innerClient = innerClient; + } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => this._innerClient.GetResponseAsync(messages, options, cancellationToken); + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable 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(); + } + + /// + /// Creates a test ChatClient implementation for testing. + /// + 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 Execute(HttpRequest request, CancellationToken cancellationToken = default) where T : ParamsBase + { + throw new NotImplementedException(); + } + + public IAnthropicClient WithOptions(Func modifier) + { + throw new NotImplementedException(); + } + } + + /// + /// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory. + /// + [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(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly. + /// + [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(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory. + /// + [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(); + Assert.NotNull(retrievedTestClient); + Assert.Same(testChatClient, retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent without clientFactory works normally. + /// + [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(); + Assert.Null(retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent with null clientFactory works normally. + /// + [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(); + Assert.Null(retrievedTestClient); + } + + /// + /// Verify that CreateAIAgent throws ArgumentNullException when client is null. + /// + [Fact] + public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException() + { + // Act & Assert + var exception = Assert.Throws(() => + ((TestAnthropicChatClient)null!).CreateAIAgent("test-model")); + + Assert.Equal("client", exception.ParamName); + } + + /// + /// Verify that CreateAIAgent with options throws ArgumentNullException when options is null. + /// + [Fact] + public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException() + { + // Arrange + var chatClient = new TestAnthropicChatClient(); + + // Act & Assert + var exception = Assert.Throws(() => + chatClient.CreateAIAgent((ChatClientAgentOptions)null!)); + + Assert.Equal("options", exception.ParamName); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj new file mode 100644 index 0000000000..291c56f879 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj @@ -0,0 +1,11 @@ + + + + true + + + + + + +