diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 3c98835c61..5d4a80026e 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -11,8 +11,8 @@ - - + + 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 index 002bd066fe..eb29d1d310 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj @@ -11,6 +11,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index 531b56e1a1..df070c335b 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -2,10 +2,9 @@ // 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 Anthropic.Foundry; using Azure.Core; using Azure.Identity; using Microsoft.Agents.AI; @@ -15,8 +14,8 @@ // 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"); +string? resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE"); +string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"); const string JokerInstructions = "You are good at telling jokes."; const string JokerName = "JokerAgent"; @@ -24,8 +23,8 @@ 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 + ? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication + : new AnthropicFoundryClient(new AnthropicAzureTokenCredential(new AzureCliCredential(), resource)); // Otherwise, use Foundry with Azure Client authentication AIAgent agent = client.CreateAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName); @@ -35,67 +34,41 @@ namespace Sample { /// - /// Provides methods for invoking the Azure hosted Anthropic api. + /// Provides methods for invoking the Azure hosted Anthropic models using types. /// - public class AnthropicFoundryClient : AnthropicClient + public sealed class AnthropicAzureTokenCredential : IAnthropicFoundryCredentials { private readonly TokenCredential _tokenCredential; - private readonly string _resourceName; + private readonly Lock _lock = new(); + private AccessToken? _cachedAccessToken; + + /// + public string ResourceName { get; } /// - /// Creates a new instance of the . + /// 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()) + /// The service resource subdomain name to use in the anthropic azure endpoint + internal AnthropicAzureTokenCredential(TokenCredential tokenCredential, string resourceName) { - this._resourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName)); + 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 - ) + /// + public void Apply(HttpRequestMessage requestMessage) { - var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken); - - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token); + lock (this._lock) + { + // Add a 5-minute buffer to avoid using tokens that are about to expire + if (this._cachedAccessToken is null || this._cachedAccessToken.Value.ExpiresOn <= DateTimeOffset.Now.AddMinutes(5)) + { + this._cachedAccessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), CancellationToken.None); + } + } - return default; + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", this._cachedAccessToken.Value.Token); } } }