Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="10.4.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.0.2" />
<PackageVersion Include="Anthropic" Version="11.0.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Anthropic.Foundry" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,17 +14,17 @@

// 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";

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);

Expand All @@ -35,67 +34,37 @@
namespace Sample
{
/// <summary>
/// Provides methods for invoking the Azure hosted Anthropic api.
/// Provides methods for invoking the Azure hosted Anthropic models using <see cref="TokenCredential"/> types.
/// </summary>
public class AnthropicFoundryClient : AnthropicClient
public class AnthropicAzureTokenCredential : IAnthropicFoundryCredentials
Comment thread
rogerbarreto marked this conversation as resolved.
Outdated
{
private readonly TokenCredential _tokenCredential;
private readonly string _resourceName;
private AccessToken? _cachedAccessToken;

public string ResourceName { get; }
Comment thread
rogerbarreto marked this conversation as resolved.

/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// Creates a new instance of the <see cref="AnthropicAzureTokenCredential"/>.
/// </summary>
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
/// <param name="tokenCredential">The credential provider. Use any specialization of <see cref="TokenCredential"/> to get your access token in supported environments.</param>
/// <param name="options">Set of <see cref="Anthropic.Core.ClientOptions"/> client option configurations</param>
/// <exception cref="ArgumentNullException">Resource is null</exception>
/// <exception cref="ArgumentNullException">TokenCredential is null</exception>
/// <remarks>
/// Any <see cref="Anthropic.Core.ClientOptions"/> APIKey or Bearer token provided will be ignored in favor of the <see cref="TokenCredential"/> provided in the constructor
/// </remarks>
public AnthropicFoundryClient(string resourceName, TokenCredential tokenCredential, Anthropic.Core.ClientOptions? options = null) : base(options ?? new())
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
Comment thread
rogerbarreto marked this conversation as resolved.
public 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);
}

/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// </summary>
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
/// <param name="apiKeyCredential">The api key.</param>
/// <param name="options">Set of <see cref="Anthropic.Core.ClientOptions"/> client option configurations</param>
/// <exception cref="ArgumentNullException">Resource is null</exception>
/// <exception cref="ArgumentNullException">Api key is null</exception>
/// <remarks>
/// Any <see cref="Anthropic.Core.ClientOptions"/> APIKey or Bearer token provided will be ignored in favor of the <see cref="ApiKeyCredential"/> provided in the constructor
/// </remarks>
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<Anthropic.Core.ClientOptions, Anthropic.Core.ClientOptions> modifier)
=> this;

protected override ValueTask BeforeSend<T>(
HttpRequest<T> request,
HttpRequestMessage requestMessage,
CancellationToken cancellationToken
)
public void Apply(HttpRequestMessage requestMessage)
Comment thread
rogerbarreto marked this conversation as resolved.
{
var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken);

requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);
if (this._cachedAccessToken is null || this._cachedAccessToken.Value.ExpiresOn <= DateTimeOffset.Now)
Comment thread
rogerbarreto marked this conversation as resolved.
Outdated
{
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
);
}
Comment thread
rogerbarreto marked this conversation as resolved.
}
}
Loading